diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87b5b4b0..5e608893 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,8 +2,6 @@ name: Build images and upload them to ghcr.io env: - OWNER: ${{ github.repository_owner }} - REGISTRY: ghcr.io BUILDKIT_PROGRESS: plain # https://github.com/docker/metadata-action?tab=readme-ov-file#environment-variables DOCKER_METADATA_PR_HEAD_SHA: true @@ -22,7 +20,7 @@ on: jobs: build: - name: amd64 & arm64 + name: build runs-on: ${{ inputs.runsOn }} timeout-minutes: 120 @@ -39,98 +37,37 @@ jobs: - name: Checkout Repo ⚡️ uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry 🔑 uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Generate tags for images on ghcr.io 🏷️ - id: tags_template - uses: docker/metadata-action@v5 - with: - bake-target: __template__-meta - images: | - name=${{ env.REGISTRY }}/aiidalab/__template__ - tags: | - type=ref,event=pr - type=edge,branch=main - type=raw,value={{tag}},enable=${{ github.ref_type == 'tag' && ! startsWith(github.ref_name, 'v') }} - type=raw,value=aiida-${{ env.AIIDA_VERSION }},enable=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} - type=raw,value=python-${{ env.PYTHON_VERSION }},enable=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} - type=raw,value=postgresql-${{ env.PGSQL_VERSION }},enable=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} - type=match,pattern=v(\d{4}\.\d{4}(-.+)?),group=1 - - - name: Generate docker-bake metadata file. - env: - BAKE_TEMPLATE_FILE: ${{ steps.tags_template.outputs.bake-file }} - run: | - .github/workflows/merge-bake-template-target.sh ${BAKE_TEMPLATE_FILE} \ - | tee docker-bake-template-meta.json - - - name: Build amd64 images 🏗️ - id: build-amd64 + # https://docs.docker.com/build/ci/github-actions/multi-platform/#with-bake + - name: Build untagged images and push to ghcr.io 🏗️ + id: build uses: docker/bake-action@v4 with: - push: true - set: | - *.platform=linux/amd64 - files: | - docker-bake.hcl - build.json - docker-bake-template-meta.json - - - name: Get amd64 images with digests - id: bake_metadata_amd64 - env: - BAKE_METADATA: ${{ steps.build-amd64.outputs.metadata }} - run: | - .github/workflows/extract-image-names.sh >> "${GITHUB_OUTPUT}" - - - name: Set Up Python 🐍 - if: ${{ inputs.runner != 'ARM64' }} - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - - - name: Install dependencies 📦 - run: | - pip install -r requirements.txt - pip freeze - - # We run tests here to catch issues early, before running arm64 build which is slow - - name: Run tests for full-stack image - run: REGISTRY= pytest -m "not integration" --target full-stack - env: ${{ fromJSON(steps.bake_metadata_amd64.outputs.images) }} - - # Here we build arm64 images (with help of QEMU virtualization) - # and upload both amd64 and arm64 images to ghcr.io - - name: Build ARM64 and upload to ghcr.io 🍎📤 - id: build-upload - uses: docker/bake-action@v4 - with: - push: true # Using provenance to disable default attestation so it will build only desired images: # https://github.com/orgs/community/discussions/45969 provenance: false - # NOTE: linux/amd64 images are taken from previous step + push: true set: | - *.platform=linux/amd64,linux/arm64 + *.platform=linux/${{ startsWith(inputs.runsOn, 'ubuntu') && 'amd64' || 'arm64' }} + *.output=type=registry,push-by-digest=true,name-canonical=true files: | docker-bake.hcl build.json - docker-bake-template-meta.json + .github/workflows/env.hcl - - name: Set output variables + - name: Get image names with digests id: bake_metadata - run: .github/workflows/extract-image-names.sh >> "${GITHUB_OUTPUT}" + run: | + .github/workflows/extract-image-names.sh + .github/workflows/extract-image-names.sh >> "${GITHUB_OUTPUT}" env: - BAKE_METADATA: ${{ steps.build-upload.outputs.metadata }} + BAKE_METADATA: ${{ steps.build.outputs.metadata }} diff --git a/.github/workflows/env.hcl b/.github/workflows/env.hcl new file mode 100644 index 00000000..fc2b844e --- /dev/null +++ b/.github/workflows/env.hcl @@ -0,0 +1,2 @@ +# env.hcl +REGISTRY = "ghcr.io/" diff --git a/.github/workflows/extract-image-names.sh b/.github/workflows/extract-image-names.sh index af814684..50e0f8bd 100755 --- a/.github/workflows/extract-image-names.sh +++ b/.github/workflows/extract-image-names.sh @@ -7,10 +7,10 @@ set -euo pipefail # The input to this script is a JSON string passed via BAKE_METADATA env variable # Here's example input (trimmed to relevant bits): -# BAKE_META: { +# BAKE_METADATA: { # "base": { +# "base": { # "buildx.build.ref": "builder-9dc30f03-42f5-4fd5-8c9a-0d54be5ad996/builder-9dc30f03-42f5-4fd5-8c9a-0d54be5ad9960/jex1w6zvslbbomtkedn4no62l", -# "containerimage.config.digest": "sha256:b76dc61672dd0efbd586d56393d3a57f6309654e6903d738168892bc09017e8b", # "containerimage.descriptor": { # "mediaType": "application/vnd.docker.distribution.manifest.v2+json", # "digest": "sha256:8e57a52b924b67567314b8ed3c968859cad99ea13521e60bbef40457e16f391d", @@ -21,29 +21,32 @@ set -euo pipefail # } # }, # "containerimage.digest": "sha256:8e57a52b924b67567314b8ed3c968859cad99ea13521e60bbef40457e16f391d", -# "image.name": "ghcr.io/aiidalab/base:pr-439,ghcr.io/aiidalab/base:sha-a0cd2be" +# "image.name": "ghcr.io/aiidalab/base" # }, # "base-with-services": { -# "containerimage.digest": "sha256:6753a809b5b2675bf4c22408e07c1df155907a465b33c369ef93ebcb1c4fec26", -# "...": "" +# "image.name": "ghcr.io/aiidalab/base-with-services" +# "containerimage.digest": "sha256:6753a809b5b2675bf4c22408e07c1df155907a465b33c369ef93ebcb1c4fec26", +# "...": "" # } # "full-stack": { -# "containerimage.digest": "sha256:85ee91f61be1ea601591c785db038e5899d68d5fb89e07d66d9efbe8f352ee48", -# "...": "" +# "image.name": "ghcr.io/aiidalab/full-stack" +# "containerimage.digest": "sha256:85ee91f61be1ea601591c785db038e5899d68d5fb89e07d66d9efbe8f352ee48", +# "...": "" # } # "lab": { -# "containerimage.digest": "sha256:4d9be090da287fcdf2d4658bb82f78bad791ccd15dac9af594fb8306abe47e97", -# "...": "" +# "image.name": "ghcr.io/aiidalab/lab" +# "containerimage.digest": "sha256:4d9be090da287fcdf2d4658bb82f78bad791ccd15dac9af594fb8306abe47e97", +# "...": "" # } # } # # Example output (real output is on one line): # # images={ -# "BASE_IMAGE":"ghcr.io/aiidalab/base:pr-439@sha256:8e57a52b924b67567314b8ed3c968859cad99ea13521e60bbef40457e16f391d", -# "BASE_WITH_SERVICES_IMAGE":"ghcr.io/aiidalab/base-with-services:pr-439@sha256:6753a809b5b2675bf4c22408e07c1df155907a465b33c369ef93ebcb1c4fec26", -# "FULL_STACK_IMAGE":"ghcr.io/aiidalab/full-stack:pr-439@sha256:85ee91f61be1ea601591c785db038e5899d68d5fb89e07d66d9efbe8f352ee48", -# "LAB_IMAGE":"ghcr.io/aiidalab/lab:pr-439@sha256:4d9be090da287fcdf2d4658bb82f78bad791ccd15dac9af594fb8306abe47e97" +# "BASE_IMAGE":"ghcr.io/aiidalab/base@sha256:8e57a52b924b67567314b8ed3c968859cad99ea13521e60bbef40457e16f391d", +# "BASE_WITH_SERVICES_IMAGE":"ghcr.io/aiidalab/base-with-services@sha256:6753a809b5b2675bf4c22408e07c1df155907a465b33c369ef93ebcb1c4fec26", +# "FULL_STACK_IMAGE":"ghcr.io/aiidalab/full-stack@sha256:85ee91f61be1ea601591c785db038e5899d68d5fb89e07d66d9efbe8f352ee48", +# "LAB_IMAGE":"ghcr.io/aiidalab/lab@sha256:4d9be090da287fcdf2d4658bb82f78bad791ccd15dac9af594fb8306abe47e97" # } if [[ -z ${BAKE_METADATA-} ]];then diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 36d4ef42..42c7b914 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,56 +18,80 @@ concurrency: jobs: - build: + build-amd64: uses: ./.github/workflows/build.yml with: runsOn: ubuntu-22.04 test-amd64: - needs: build + needs: build-amd64 strategy: fail-fast: false matrix: - # NOTE: amd64 full-stack image is tested during build step - target: ["base", "lab", "base-with-services"] - + target: [base, full-stack] #[base, lab, base-with-services, full-stack] uses: ./.github/workflows/test.yml with: runsOn: ubuntu-22.04 - images: ${{ needs.build.outputs.images }} + images: ${{ needs.build-amd64.outputs.images }} target: ${{ matrix.target }} integration: false - # To save self-hosted runner resources, we're only testing full-stack image + # To save arm64 runner resources, we only try to build once amd64 build and tests succeed + build-arm64: + needs: [build-amd64, test-amd64] + uses: ./.github/workflows/build.yml + with: + runsOn: buildjet-4vcpu-ubuntu-2204-arm + test-arm64: - needs: build + needs: build-arm64 + strategy: + fail-fast: false + matrix: + target: [base, full-stack] #[base, lab, base-with-services, full-stack] uses: ./.github/workflows/test.yml with: - runsOn: ARM64 - images: ${{ needs.build.outputs.images }} - target: "full-stack" + runsOn: buildjet-2vcpu-ubuntu-2204-arm + images: ${{ needs.build-arm64.outputs.images }} + target: ${{ matrix.target }} integration: false test-integration: - needs: build + needs: [build-arm64, build-amd64] strategy: fail-fast: false matrix: - runner: ["ubuntu-22.04", "ARM64"] + runsOn: [ubuntu-22.04, ARM64] uses: ./.github/workflows/test.yml with: - runsOn: ${{ matrix.runner }} - images: ${{ needs.build.outputs.images }} - target: "full-stack" + runsOn: ${{ matrix.runsOn}} + images: ${{ startsWith(matrix.runsOn, 'ubuntu') && needs.build-amd64.outputs.images || needs.build-arm64.outputs.images }} + target: full-stack integration: true - publish: + publish_ghcr: + if: github.repository == 'aiidalab/aiidalab-docker-stack' + needs: [test-amd64, test-arm64] + uses: ./.github/workflows/publish_ghcr.yml + with: + runsOn: ubuntu-22.04 + registry: ghcr.io + images_amd64: ${{ needs.test_amd64.inputs.images }} + images_arm64: ${{ needs.test_arm64.inputs.images }} + secrets: + REGISTRY_USERNAME: ${{ github.actor }} + REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish_docker: if: >- github.repository == 'aiidalab/aiidalab-docker-stack' && (github.ref_type == 'tag' || github.ref_name == 'main') - needs: [build, test-amd64, test-arm64] + needs: [publish_ghcr] uses: ./.github/workflows/publish.yml with: runsOn: ubuntu-22.04 - images: ${{ needs.build.outputs.images }} - secrets: inherit + registry: docker.io + images: ${{ needs.publish_ghcr.outputs.images }} + secrets: + REGISTRY_USERNAME: ${{ secrets.DOCKER_USERNAME }} + REGISTRY_TOKEN: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/merge-bake-template-target.sh b/.github/workflows/merge-bake-template-target.sh deleted file mode 100755 index 6443865e..00000000 --- a/.github/workflows/merge-bake-template-target.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# For each target that is part of the "default" group, replace the term -# "__template__" in the provided template bake-file, and then merge all -# resulting bake-files into one. -# -# That means if the default group contains a target named "base", the script -# will replace all occurrences of the term "__template__" with "base" and then -# merge the result with those for all other targets part of the "default" group. -# -# The motivation is to be able to use a bake-file generated by the -# docker/meta-action (which can currently only handle a single bake-target) for -# all targets currently specified in the main bake-file ("docker-bake.hcl"). -# -# Example output (trimmed): -# { -# "target": { -# "base-meta": { -# "tags": [ -# "ghcr.io/aiidalab/base:pr-439", -# "ghcr.io/aiidalab/base:sha-40bdfc9" -# ], -# "labels": { -# "org.opencontainers.image.created": "2024-04-19T11:50:09.021Z", -# "org.opencontainers.image.description": "Docker images with the basic software stack for AiiDAlab", -# "org.opencontainers.image.licenses": "NOASSERTION", -# "org.opencontainers.image.revision": "40bdfc9491e409c67320ca3566f85aaf3feb36ea", -# "org.opencontainers.image.source": "https://github.com/aiidalab/aiidalab-docker-stack", -# "org.opencontainers.image.title": "aiidalab-docker-stack", -# "org.opencontainers.image.url": "https://github.com/aiidalab/aiidalab-docker-stack", -# "org.opencontainers.image.version": "pr-439" -# }, -# "args": { -# "DOCKER_META_IMAGES": "ghcr.io/aiidalab/base", -# "DOCKER_META_VERSION": "pr-439" -# } -# }, -# "base-with-services-meta": { -# "tags": [ -# "ghcr.io/aiidalab/base-with-services:pr-439", -# "ghcr.io/aiidalab/base-with-services:sha-40bdfc9" -# ], -# "labels": {}, -# "args": { -# "DOCKER_META_IMAGES": "ghcr.io/aiidalab/base-with-services", -# "DOCKER_META_VERSION": "pr-439" -# } -# }, -# "lab-meta": { -# "tags": [ -# "ghcr.io/aiidalab/lab:pr-439", -# "ghcr.io/aiidalab/lab:sha-40bdfc9" -# ], -# "labels": {}, -# "args": { -# "DOCKER_META_IMAGES": "ghcr.io/aiidalab/lab", -# "DOCKER_META_VERSION": "pr-439" -# } -# }, -# "full-stack-meta": { -# "tags": [ -# "ghcr.io/aiidalab/full-stack:pr-439", -# "ghcr.io/aiidalab/full-stack:sha-40bdfc9" -# ], -# "labels": {}, -# "args": { -# "DOCKER_META_IMAGES": "ghcr.io/aiidalab/full-stack", -# "DOCKER_META_VERSION": "pr-439" -# } -# } -# } -#} - -if [[ -z ${1-} ]];then - echo "ERROR: Provide path to bake-file template as first parameter" - exit 1 -fi - -input_file=$1 -if [[ ! -f ${input_file} ]];then - echo "ERROR: File $input_file does not exist!" - exit 1 -fi - -# Flatten the json file into a single line -input=$(cat $input_file | jq -c) - -# Determine the targets. -# TODO: This currently fails due to PYTHON_MINOR_VERSION computation, -# let's just hardcode for now -# TARGETS=$(docker buildx bake --print | jq -cr '.group.default.targets' | jq -r '.[]') -TARGETS="base base-with-services lab full-stack" - -# Generate the meta JSON strings -meta="" -for target in $TARGETS; do - meta="${meta} ${input//__template__/${target}}" -done - -# Combine into merged bake file. -echo $meta | jq -s 'reduce .[] as $x ({}; . * $x)' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6fd22a8c..6888a2f8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,20 +9,29 @@ on: required: true type: string images: - description: Images built in build step + description: multiarch images built in previous build step + required: false + type: string + registry: + description: Container registry to publish Docker images required: true type: string + secrets: + REGISTRY_USERNAME: + required: true + REGISTRY_TOKEN: + required: true jobs: release: - name: DockerHub release + name: ${{ inputs.registry }} release runs-on: ${{ inputs.runsOn }} timeout-minutes: 30 strategy: fail-fast: true matrix: - target: ["base", "base-with-services", "lab", "full-stack"] + target: ["base"] #, "base-with-services", "lab", "full-stack"] steps: - uses: actions/checkout@v4 @@ -37,9 +46,9 @@ jobs: - name: Login to DockerHub 🔑 uses: docker/login-action@v3 with: - registry: docker.io - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + registry: ${{ inputs.registry }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} - name: Read build variables id: build_vars @@ -53,8 +62,9 @@ jobs: uses: docker/metadata-action@v5 env: ${{ fromJSON(steps.build_vars.outputs.vars) }} with: - images: docker.io/aiidalab/${{ matrix.target }} + images: ${{ inputs.registry }}/aiidalab/${{ matrix.target }} tags: | + type=ref,event=pr type=edge,enable={{is_default_branch}} type=raw,value={{tag}},enable=${{ github.ref_type == 'tag' && ! startsWith(github.ref_name, 'v') }} type=raw,value=aiida-${{ env.AIIDA_VERSION }},enable=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} @@ -64,6 +74,7 @@ jobs: - name: Determine src image tag id: images + if: inputs.registry == 'docker.io' run: | src=$(echo '${{ inputs.images }}'| jq -cr '.[("${{ matrix.target }}"|ascii_upcase|sub("-"; "_"; "g")) + "_IMAGE"]') echo "src=$src" @@ -71,12 +82,14 @@ jobs: - name: Push image uses: akhilerm/tag-push-action@v2.2.0 + if: inputs.registry == 'docker.io' with: src: ${{ steps.images.outputs.src }} dst: ${{ steps.meta.outputs.tags }} - name: Docker Hub Description uses: peter-evans/dockerhub-description@v4 + if: inputs.registry == 'docker.io' with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -85,6 +98,6 @@ jobs: - uses: softprops/action-gh-release@v0.1.14 name: Create release - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') && inputs.registry == 'docker.io' with: generate_release_notes: true diff --git a/.github/workflows/publish_ghcr.yml b/.github/workflows/publish_ghcr.yml new file mode 100644 index 00000000..a8315899 --- /dev/null +++ b/.github/workflows/publish_ghcr.yml @@ -0,0 +1,103 @@ +--- +name: Publish images to DockerHub + +on: + workflow_call: + inputs: + runsOn: + description: GitHub Actions Runner image + required: true + type: string + images_amd64: + description: amd64 images built in build step + required: false + type: string + images_arm64: + description: arm64 images built in build step + required: false + type: string + registry: + description: Container registry to publish Docker images + required: true + type: string + outputs: + images: + description: Published multiarch images + value: ${{ jobs.build.outputs.images }} + secrets: + REGISTRY_USERNAME: + required: true + REGISTRY_TOKEN: + required: true + +jobs: + + release: + name: ${{ inputs.registry }} release + runs-on: ${{ inputs.runsOn }} + timeout-minutes: 30 + outputs: + images: "TODO" + strategy: + fail-fast: true + matrix: + target: ["base"] #, "base-with-services", "lab", "full-stack"] + + steps: + - uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry 🔑 + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Read build variables + id: build_vars + run: | + vars=$(cat build.json | jq -c '[.variable | to_entries[] | {"key": .key, "value": .value.default}] | from_entries') + echo "vars=$vars" + echo "vars=$vars" >> "${GITHUB_OUTPUT}" + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + env: ${{ fromJSON(steps.build_vars.outputs.vars) }} + with: + images: ${{ inputs.registry }}/aiidalab/${{ matrix.target }} + tags: | + type=ref,event=pr + type=edge,enable={{is_default_branch}} + type=raw,value={{tag}},enable=${{ github.ref_type == 'tag' && ! startsWith(github.ref_name, 'v') }} + type=raw,value=aiida-${{ env.AIIDA_VERSION }},enable=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} + type=raw,value=python-${{ env.PYTHON_VERSION }},enable=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} + type=raw,value=postgresql-${{ env.PGSQL_VERSION }},enable=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} + type=match,pattern=v(\d{4}\.\d{4}(-.+)?),group=1 + + - name: Determine src image tags + id: images + run: | + src_amd64=$(echo '${{ inputs.images_amd64 }}'| jq -cr '.[("${{ matrix.target }}"|ascii_upcase|sub("-"; "_"; "g")) + "_IMAGE"]') + src_arm64=$(echo '${{ inputs.images_arm64 }}'| jq -cr '.[("${{ matrix.target }}"|ascii_upcase|sub("-"; "_"; "g")) + "_IMAGE"]') + echo "src_amd64=$src_amd64" + echo "src_arm64=$src_arm64" + echo "src_amd64=$src" >> "${GITHUB_OUTPUT}" + echo "src_arm64=$src" >> "${GITHUB_OUTPUT}" + + - name: Merge tags for the images of different archs + id: merge + if: false + run: | + docker manifest create ${{ steps.images.src_amd64 }} + docker manifest push ${{ steps.images.src_amd64 }} + docker manifest create ${{ steps.images.src_arm64 }} + docker manifest push ${{ steps.images.src_arm64 }} + shell: bash + + - name: Push image + uses: akhilerm/tag-push-action@v2.2.0 + if: false + with: + src: ${{ steps.merge.outputs.src }} + dst: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f9a917b..9fb79317 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Set Up Python 🐍 - if: ${{ inputs.runsOn != 'ARM64' }} + if: ${{ startsWith(inputs.runsOn, 'ubuntu') }} uses: actions/setup-python@v5 with: python-version: '3.11' diff --git a/docker-bake.hcl b/docker-bake.hcl index 5fe77879..809a30d8 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -26,7 +26,6 @@ variable "ORGANIZATION" { } variable "REGISTRY" { - default = "docker.io" } variable "PLATFORMS" { @@ -37,10 +36,11 @@ variable "TARGETS" { default = ["base", "base-with-services", "lab", "full-stack"] } +# TODO: Automatically handle both empty and non-empty VERSION function "tags" { params = [image] result = [ - "${REGISTRY}/${ORGANIZATION}/${image}:${VERSION}", + "${REGISTRY}${ORGANIZATION}/${image}${VERSION}", ] }