diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 2fd486cd..6027684d 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -17,6 +17,11 @@ stardoc_with_diff_test(
bzl_library_target = "//oci/private:image_index",
)
+stardoc_with_diff_test(
+ name = "push",
+ bzl_library_target = "//oci/private:push",
+)
+
stardoc_with_diff_test(
name = "cosign_sign",
bzl_library_target = "//cosign/private:sign",
diff --git a/docs/push.md b/docs/push.md
new file mode 100644
index 00000000..46238344
--- /dev/null
+++ b/docs/push.md
@@ -0,0 +1,80 @@
+
+
+Implementation details for the push rule
+
+
+
+## oci_push
+
+
+oci_push(name, default_tags, image, repository)
+
+
+Push an oci_image or oci_image_index to a remote registry.
+
+Pushing and tagging are performed sequentially which MAY lead to non-atomic pushes if one the following events occur;
+
+- Remote registry rejects a tag due to various reasons. eg: forbidden characters, existing tags
+- Remote registry closes the connection during the tagging
+- Local network outages
+
+In order to avoid incomplete pushes oci_push will push the image by its digest and then apply the `default_tags` sequentially at
+the remote registry.
+
+Any failure during pushing or tagging will be reported with non-zero exit code cause remaining steps to be skipped.
+
+
+Push an oci_image to docker registry with latest tag
+
+```starlark
+oci_image(name = "image")
+
+oci_push(
+ image = ":image",
+ repository = "index.docker.io//image",
+ default_tags = ["latest"]
+)
+```
+
+Push an oci_image_index to github container registry with a semver tag
+
+```starlark
+oci_image(name = "app_linux_arm64")
+
+oci_image(name = "app_linux_amd64")
+
+oci_image(name = "app_windows_amd64")
+
+oci_image_index(
+ name = "app_image",
+ images = [
+ ":app_linux_arm64",
+ ":app_linux_amd64",
+ ":app_windows_amd64",
+ ]
+)
+
+oci_push(
+ image = ":app_image",
+ repository = "ghcr.io//image",
+ default_tags = ["0.0.0"]
+)
+```
+
+Ideally the semver information is gathered from a vcs, like git, instead of being hardcoded to the BUILD files.
+However, due to nature of BUILD files being static, one has to use `-t|--tag` flag to pass the tag at runtime instead of using `default_tags`. eg. `bazel run //target:push -- --tag $(git tag)`
+
+Similary, the `repository` attribute can be overridden at runtime with the `-r|--repository` flag. eg. `bazel run //target:push -- --repository index.docker.io//image`
+
+
+**ATTRIBUTES**
+
+
+| Name | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| name | A unique name for this target. | Name | required | |
+| default_tags | List of tags to apply to the image at remote registry. | List of strings | optional | [] |
+| image | Label to an oci_image or oci_image_index | Label | optional | None |
+| repository | Repository URL where the image will be signed at. eg: index.docker.io/<user>/image. digests and tags are disallowed. | String | required | |
+
+
diff --git a/example/push/BUILD.bazel b/example/push/BUILD.bazel
new file mode 100644
index 00000000..0d74517d
--- /dev/null
+++ b/example/push/BUILD.bazel
@@ -0,0 +1,59 @@
+load("//oci:defs.bzl", "oci_image", "oci_image_index", "oci_push")
+
+oci_image(
+ name = "image",
+ architecture = "amd64",
+ entrypoint = ["/fail"],
+ os = "linux",
+)
+
+oci_push(
+ name = "push_image",
+ image = ":image",
+ repository = "index.docker.io//image",
+ default_tags = ["latest"]
+)
+
+oci_push(
+ name = "push_image_wo_tags",
+ image = ":image",
+ repository = "index.docker.io//image"
+)
+
+oci_image_index(
+ name = "image_index",
+ images = [
+ ":image"
+ ]
+)
+
+oci_push(
+ name = "push_image_index",
+ image = ":image_index",
+ repository = "index.docker.io//image",
+ default_tags = ["nightly"]
+)
+
+
+sh_test(
+ name = "test",
+ srcs = ["test.bash"],
+ args = [
+ "$(CRANE_BIN)",
+ "$(LAUNCHER)",
+ "$(location :push_image)",
+ "$(location :push_image_index)",
+ "$(location :push_image_wo_tags)"
+ ],
+ data = [
+ ":push_image",
+ ":push_image_index",
+ ":push_image_wo_tags",
+ "@oci_crane_toolchains//:current_toolchain",
+ "@oci_zot_toolchains//:current_toolchain",
+ ],
+ toolchains = [
+ "@oci_crane_toolchains//:current_toolchain",
+ "@oci_zot_toolchains//:current_toolchain",
+ ],
+)
diff --git a/example/push/test.bash b/example/push/test.bash
new file mode 100755
index 00000000..1800a8d8
--- /dev/null
+++ b/example/push/test.bash
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+set -o pipefail -o errexit -o nounset
+
+readonly CRANE="$1"
+readonly REGISTRY_LAUNCHER="$2"
+
+# Launch a registry instance at a random port
+source "${REGISTRY_LAUNCHER}"
+start_registry $TEST_TMPDIR $TEST_TMPDIR/output.log
+echo "Registry is running at ${REGISTRY}"
+
+
+readonly PUSH_IMAGE="$3"
+readonly PUSH_IMAGE_INDEX="$4"
+readonly PUSH_IMAGE_WO_TAGS="$5"
+
+
+# should push image with default tags
+REPOSITORY="${REGISTRY}/local"
+"${PUSH_IMAGE}" --repository "${REPOSITORY}"
+"${CRANE}" digest "$REPOSITORY:latest"
+
+# should push image_index with default tags
+REPOSITORY="${REGISTRY}/local-index"
+"${PUSH_IMAGE_INDEX}" --repository "${REPOSITORY}"
+"${CRANE}" digest "$REPOSITORY:nightly"
+
+
+# should push image without default tags
+REPOSITORY="${REGISTRY}/local-wo-tags"
+"${PUSH_IMAGE_WO_TAGS}" --repository "${REPOSITORY}"
+TAGS=$("${CRANE}" ls "$REPOSITORY")
+if [ -n "${TAGS}" ]; then
+ echo "image is not supposed to have any tags but got"
+ echo "${TAGS}"
+ exit 1
+fi
+
+# should push image with the --tag flag.
+REPOSITORY="${REGISTRY}/local-flag-tag"
+"${PUSH_IMAGE_WO_TAGS}" --repository "${REPOSITORY}" --tag "custom"
+TAGS=$("${CRANE}" ls "$REPOSITORY")
+if [ "${TAGS}" != "custom" ]; then
+ echo "image is supposed to have custom tag but got"
+ echo "${TAGS}"
+ exit 1
+fi
diff --git a/oci/defs.bzl b/oci/defs.bzl
index aa1125ee..c93a2244 100644
--- a/oci/defs.bzl
+++ b/oci/defs.bzl
@@ -3,9 +3,11 @@
load("//oci/private:tarball.bzl", _oci_tarball = "oci_tarball")
load("//oci/private:image.bzl", _oci_image = "oci_image")
load("//oci/private:image_index.bzl", _oci_image_index = "oci_image_index")
+load("//oci/private:push.bzl", _oci_push = "oci_push")
load("//oci/private:structure_test.bzl", _structure_test = "structure_test")
oci_tarball = _oci_tarball
oci_image = _oci_image
oci_image_index = _oci_image_index
+oci_push = _oci_push
structure_test = _structure_test
diff --git a/oci/private/BUILD.bazel b/oci/private/BUILD.bazel
index e875561e..41945a3a 100644
--- a/oci/private/BUILD.bazel
+++ b/oci/private/BUILD.bazel
@@ -9,6 +9,7 @@ exports_files([
"image.sh.tpl",
"image_index.sh.tpl",
"tarball.sh.tpl",
+ "push.sh.tpl"
])
filegroup(
@@ -48,6 +49,15 @@ bzl_library(
],
)
+bzl_library(
+ name = "push",
+ srcs = ["push.bzl"],
+ visibility = [
+ "//docs:__pkg__",
+ "//oci:__subpackages__",
+ ],
+)
+
bzl_library(
name = "toolchains_repo",
srcs = ["toolchains_repo.bzl"],
diff --git a/oci/private/push.bzl b/oci/private/push.bzl
new file mode 100644
index 00000000..8b2185f9
--- /dev/null
+++ b/oci/private/push.bzl
@@ -0,0 +1,110 @@
+"Implementation details for the push rule"
+
+_DOC = """Push an oci_image or oci_image_index to a remote registry.
+
+Pushing and tagging are performed sequentially which MAY lead to non-atomic pushes if one the following events occur;
+
+- Remote registry rejects a tag due to various reasons. eg: forbidden characters, existing tags
+- Remote registry closes the connection during the tagging
+- Local network outages
+
+In order to avoid incomplete pushes oci_push will push the image by its digest and then apply the `default_tags` sequentially at
+the remote registry.
+
+Any failure during pushing or tagging will be reported with non-zero exit code cause remaining steps to be skipped.
+
+
+Push an oci_image to docker registry with latest tag
+
+```starlark
+oci_image(name = "image")
+
+oci_push(
+ image = ":image",
+ repository = "index.docker.io//image",
+ default_tags = ["latest"]
+)
+```
+
+Push an oci_image_index to github container registry with a semver tag
+
+```starlark
+oci_image(name = "app_linux_arm64")
+
+oci_image(name = "app_linux_amd64")
+
+oci_image(name = "app_windows_amd64")
+
+oci_image_index(
+ name = "app_image",
+ images = [
+ ":app_linux_arm64",
+ ":app_linux_amd64",
+ ":app_windows_amd64",
+ ]
+)
+
+oci_push(
+ image = ":app_image",
+ repository = "ghcr.io//image",
+ default_tags = ["0.0.0"]
+)
+```
+
+Ideally the semver information is gathered from a vcs, like git, instead of being hardcoded to the BUILD files.
+However, due to nature of BUILD files being static, one has to use `-t|--tag` flag to pass the tag at runtime instead of using `default_tags`. eg. `bazel run //target:push -- --tag $(git tag)`
+
+Similary, the `repository` attribute can be overridden at runtime with the `-r|--repository` flag. eg. `bazel run //target:push -- --repository index.docker.io//image`
+"""
+_attrs = {
+ "image": attr.label(allow_single_file = True, doc = "Label to an oci_image or oci_image_index"),
+ "repository": attr.string(mandatory = True, doc = "Repository URL where the image will be signed at. eg: index.docker.io//image. digests and tags are disallowed."),
+ "default_tags": attr.string_list(doc = "List of tags to apply to the image at remote registry."),
+ "_push_sh_tpl": attr.label(default = "push.sh.tpl", allow_single_file = True),
+}
+
+def _quote_args(args):
+ return ["\"{}\"".format(arg) for arg in args]
+
+def _impl(ctx):
+ crane = ctx.toolchains["@contrib_rules_oci//oci:crane_toolchain_type"]
+ jq = ctx.toolchains["@aspect_bazel_lib//lib:yq_toolchain_type"]
+
+ if not ctx.file.image.is_directory:
+ fail("image attribute must be a oci_image or oci_image_index")
+
+ if ctx.attr.repository.find(":") != -1 or ctx.attr.repository.find("@") != -1:
+ fail("repository attribute should not contain digest or tag.")
+
+ fixed_args = ["--tag={}".format(tag) for tag in ctx.attr.default_tags]
+ fixed_args.extend(["--repository", ctx.attr.repository])
+
+ executable = ctx.actions.declare_file("push_%s.sh" % ctx.label.name)
+ ctx.actions.expand_template(
+ template = ctx.file._push_sh_tpl,
+ output = executable,
+ is_executable = True,
+ substitutions = {
+ "{{crane_path}}": crane.crane_info.crane_path,
+ "{{yq_path}}": jq.yqinfo.bin.short_path,
+ "{{image_dir}}": ctx.file.image.short_path,
+ "{{fixed_args}}": " ".join(_quote_args(fixed_args)),
+ },
+ )
+
+ runfiles = ctx.runfiles(files = [ctx.file.image])
+ runfiles = runfiles.merge(jq.default.default_runfiles)
+ runfiles = runfiles.merge(crane.default.default_runfiles)
+
+ return DefaultInfo(executable = executable, runfiles = runfiles)
+
+oci_push = rule(
+ implementation = _impl,
+ attrs = _attrs,
+ doc = _DOC,
+ executable = True,
+ toolchains = [
+ "@contrib_rules_oci//oci:crane_toolchain_type",
+ "@aspect_bazel_lib//lib:yq_toolchain_type",
+ ],
+)
diff --git a/oci/private/push.sh.tpl b/oci/private/push.sh.tpl
new file mode 100644
index 00000000..a420c83e
--- /dev/null
+++ b/oci/private/push.sh.tpl
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+set -o pipefail -o errexit -o nounset
+
+readonly CRANE="{{crane_path}}"
+readonly YQ="{{yq_path}}"
+readonly IMAGE_DIR="{{image_dir}}"
+readonly FIXED_ARGS=({{fixed_args}})
+
+# set $@ to be FIXED_ARGS+$@
+ALL_ARGS=(${FIXED_ARGS[@]} $@)
+set -- ${ALL_ARGS[@]}
+
+REPOSITORY="{{repository}}"
+TAGS=()
+ARGS=()
+
+while (( $# > 0 )); do
+ case $1 in
+ (-t|--tag)
+ TAGS+=( "$2" )
+ shift
+ shift;;
+ (--tag=*)
+ TAGS+=( "${1#--tag=}" )
+ shift;;
+ (-r|--repository)
+ REPOSITORY="$2"
+ shift
+ shift;;
+ (--repository=*)
+ REPOSITORY="${1#--repository=}"
+ shift;;
+ (*)
+ ARGS+=( "$1" )
+ shift;;
+ esac
+done
+
+DIGEST=$("${YQ}" eval '.manifests[0].digest' "${IMAGE_DIR}/index.json")
+
+REFS=$(mktemp)
+"${CRANE}" push "${IMAGE_DIR}" "${REPOSITORY}@${DIGEST}" "${ARGS[@]+"${ARGS[@]}"}" --image-refs "${REFS}"
+
+for tag in "${TAGS[@]+"${TAGS[@]}"}"
+do
+ "${CRANE}" tag $(cat "${REFS}") "${tag}"
+done
\ No newline at end of file