From a23fc0a86717372f8408d67c728793505ff8c6e8 Mon Sep 17 00:00:00 2001 From: Declan Carroll Date: Wed, 8 Apr 2026 07:12:02 +0100 Subject: [PATCH] ci: Tag Docker images (#28088) --- .github/scripts/docker/docker-tags.mjs | 26 ++++++++++++++++++++++--- .github/workflows/docker-build-push.yml | 25 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/.github/scripts/docker/docker-tags.mjs b/.github/scripts/docker/docker-tags.mjs index 8f1f7dac631..a6a2841653b 100644 --- a/.github/scripts/docker/docker-tags.mjs +++ b/.github/scripts/docker/docker-tags.mjs @@ -9,7 +9,7 @@ class TagGenerator { this.githubOutput = process.env.GITHUB_OUTPUT || null; } - generate({ image, version, platform, includeDockerHub = false }) { + generate({ image, version, platform, includeDockerHub = false, sha = '' }) { let imageName = image; let versionSuffix = ''; @@ -27,6 +27,21 @@ class TagGenerator { }; tags.all = [...tags.ghcr, ...tags.docker]; + + // Generate additional SHA-based tags for immutable references + if (sha) { + const shaVersion = `${version}-${sha}`; + const shaPlatformTag = `${shaVersion}${versionSuffix}${platformSuffix}`; + const shaGhcr = [`ghcr.io/${this.githubOwner}/${imageName}:${shaPlatformTag}`]; + const shaDocker = includeDockerHub + ? [`${this.dockerUsername}/${imageName}:${shaPlatformTag}`] + : []; + tags.all = [...tags.all, ...shaGhcr, ...shaDocker]; + tags.ghcr = [...tags.ghcr, ...shaGhcr]; + tags.docker = [...tags.docker, ...shaDocker]; + tags.shaPrimaryTag = shaGhcr[0].replace(/-amd64$|-arm64$/, ''); + } + return tags; } @@ -40,18 +55,21 @@ class TagGenerator { `${prefixStr}docker_tag=${tags.docker[0] || ''}`, `${prefixStr}primary_tag=${primaryTag}`, ]; + if (tags.shaPrimaryTag) { + outputs.push(`${prefixStr}sha_primary_tag=${tags.shaPrimaryTag}`); + } appendFileSync(this.githubOutput, outputs.join('\n') + '\n'); } else { console.log(JSON.stringify(tags, null, 2)); } } - generateAll({ version, platform, includeDockerHub = false }) { + generateAll({ version, platform, includeDockerHub = false, sha = '' }) { const images = ['n8n', 'runners', 'runners-distroless']; const results = {}; for (const image of images) { - const tags = this.generate({ image, version, platform, includeDockerHub }); + const tags = this.generate({ image, version, platform, includeDockerHub, sha }); const prefix = image.replace('-distroless', '_distroless'); results[prefix] = tags; @@ -86,6 +104,7 @@ if (import.meta.url === `file://${process.argv[1]}`) { version, platform: getArg('platform'), includeDockerHub: hasFlag('include-docker'), + sha: getArg('sha') || '', }); if (!generator.githubOutput) { console.log(JSON.stringify(results, null, 2)); @@ -101,6 +120,7 @@ if (import.meta.url === `file://${process.argv[1]}`) { version, platform: getArg('platform'), includeDockerHub: hasFlag('include-docker'), + sha: getArg('sha') || '', }); generator.output(tags); } diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 28ea84b6cf0..5f26e326b2e 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -79,6 +79,9 @@ jobs: primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.n8n_primary_tag }} runners_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.runners_primary_tag }} runners_distroless_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.runners_distroless_primary_tag }} + n8n_sha_manifest_tag: ${{ steps.determine-tags.outputs.n8n_sha_primary_tag }} + runners_sha_manifest_tag: ${{ steps.determine-tags.outputs.runners_sha_primary_tag }} + runners_distroless_sha_manifest_tag: ${{ steps.determine-tags.outputs.runners_distroless_sha_primary_tag }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -101,6 +104,7 @@ jobs: --all \ --version "${{ needs.determine-build-context.outputs.n8n_version }}" \ --platform "${{ matrix.docker_platform }}" \ + --sha "${GITHUB_SHA::7}" \ ${{ needs.determine-build-context.outputs.push_to_docker == 'true' && '--include-docker' || '' }} echo "=== Generated Docker Tags ===" @@ -228,6 +232,11 @@ jobs: create_manifest "runners" "${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}" create_manifest "runners-distroless" "${{ needs.build-and-push-docker.outputs.runners_distroless_primary_ghcr_manifest_tag }}" + # Create SHA-tagged manifests (immutable references for deployments) + create_manifest "n8n (sha)" "${{ needs.build-and-push-docker.outputs.n8n_sha_manifest_tag }}" + create_manifest "runners (sha)" "${{ needs.build-and-push-docker.outputs.runners_sha_manifest_tag }}" + create_manifest "runners-distroless (sha)" "${{ needs.build-and-push-docker.outputs.runners_distroless_sha_manifest_tag }}" + - name: Create Docker Hub manifests if: needs.determine-build-context.outputs.push_to_docker == 'true' run: | @@ -241,6 +250,8 @@ jobs: ["runners-distroless"]="${VERSION}-distroless" ) + SHORT_SHA="${GITHUB_SHA::7}" + for image in "${!images[@]}"; do TAG_SUFFIX="${images[$image]}" IMAGE_NAME="${image//-distroless/}" # Remove -distroless from image name @@ -250,6 +261,20 @@ jobs: --tag "${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}" \ "${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}-amd64" \ "${DOCKER_BASE}/${IMAGE_NAME}:${TAG_SUFFIX}-arm64" + + # Create SHA-tagged manifest (immutable reference) + # For distroless, insert SHA between version and -distroless suffix + # to match docker-tags.mjs format: nightly-abc1234-distroless (not nightly-distroless-abc1234) + if [[ "$image" == *"-distroless"* ]]; then + SHA_SUFFIX="${VERSION}-${SHORT_SHA}-distroless" + else + SHA_SUFFIX="${TAG_SUFFIX}-${SHORT_SHA}" + fi + echo "Creating Docker Hub SHA manifest for $image: ${SHA_SUFFIX}" + docker buildx imagetools create \ + --tag "${DOCKER_BASE}/${IMAGE_NAME}:${SHA_SUFFIX}" \ + "${DOCKER_BASE}/${IMAGE_NAME}:${SHA_SUFFIX}-amd64" \ + "${DOCKER_BASE}/${IMAGE_NAME}:${SHA_SUFFIX}-arm64" done - name: Get manifest digests for attestation