feat: Publish a pointer-compressed n8n image variant (#36720)

This commit is contained in:
Iván Ovejero
2026-08-21 08:58:01 +00:00
committed by GitHub
parent 1c1367b4ba
commit 63d8747070
6 changed files with 110 additions and 9 deletions
+1
View File
@@ -27,6 +27,7 @@ const ALLOW_REFS = [
export function parseTargets(env) {
return [
{ label: 'n8n', image: env.N8N_IMAGE, digest: env.N8N_DIGEST },
{ label: 'n8n-pc', image: env.N8N_PC_IMAGE, digest: env.N8N_PC_DIGEST },
{ label: 'runners', image: env.RUNNERS_IMAGE, digest: env.RUNNERS_DIGEST },
{ label: 'runners-distroless', image: env.DISTROLESS_IMAGE, digest: env.DISTROLESS_DIGEST },
].filter((t) => t.image && t.digest);
+7 -2
View File
@@ -18,6 +18,11 @@ class TagGenerator {
versionSuffix = '-distroless';
}
if (image === 'n8n-pc') {
imageName = 'n8n';
versionSuffix = '-pc';
}
const platformSuffix = platform ? `-${platform.split('/').pop()}` : '';
const fullVersion = `${version}${versionSuffix}${platformSuffix}`;
@@ -82,12 +87,12 @@ class TagGenerator {
}
generateAll({ version, platform, includeDockerHub = false, sha = '', date = '' }) {
const images = ['n8n', 'runners', 'runners-distroless'];
const images = ['n8n', 'n8n-pc', 'runners', 'runners-distroless'];
const results = {};
for (const image of images) {
const tags = this.generate({ image, version, platform, includeDockerHub, sha, date });
const prefix = image.replace('-distroless', '_distroless');
const prefix = image.replaceAll('-', '_');
results[prefix] = tags;
if (this.githubOutput) {
@@ -7,6 +7,7 @@
*
* Environment variables:
* N8N_TAG - Full image reference for n8n image
* N8N_PC_TAG - Full image reference for the pointer-compressed n8n image
* RUNNERS_TAG - Full image reference for runners image
* DISTROLESS_TAG - Full image reference for runners-distroless image
* GITHUB_OUTPUT - Path to GitHub Actions output file (optional)
@@ -36,17 +37,21 @@ function setOutput(name, value) {
}
const n8nTag = process.env.N8N_TAG || '';
const n8nPcTag = process.env.N8N_PC_TAG || '';
const runnersTag = process.env.RUNNERS_TAG || '';
const distrolessTag = process.env.DISTROLESS_TAG || '';
const results = {
n8n: { digest: getDigest(n8nTag), image: getImageName(n8nTag) },
n8n_pc: { digest: getDigest(n8nPcTag), image: getImageName(n8nPcTag) },
runners: { digest: getDigest(runnersTag), image: getImageName(runnersTag) },
runners_distroless: { digest: getDigest(distrolessTag), image: getImageName(distrolessTag) },
};
setOutput('n8n_digest', results.n8n.digest);
setOutput('n8n_image', results.n8n.image);
setOutput('n8n_pc_digest', results.n8n_pc.digest);
setOutput('n8n_pc_image', results.n8n_pc.image);
setOutput('runners_digest', results.runners.digest);
setOutput('runners_image', results.runners.image);
setOutput('runners_distroless_digest', results.runners_distroless.digest);
@@ -54,10 +59,12 @@ setOutput('runners_distroless_image', results.runners_distroless.image);
console.log('=== Manifest Digests ===');
console.log(`n8n: ${results.n8n.digest || 'N/A'}`);
console.log(`n8n-pc: ${results.n8n_pc.digest || 'N/A'}`);
console.log(`runners: ${results.runners.digest || 'N/A'}`);
console.log(`runners-distroless: ${results.runners_distroless.digest || 'N/A'}`);
console.log('');
console.log('=== Image Names ===');
console.log(`n8n: ${results.n8n.image || 'N/A'}`);
console.log(`n8n-pc: ${results.n8n_pc.image || 'N/A'}`);
console.log(`runners: ${results.runners.image || 'N/A'}`);
console.log(`runners-distroless: ${results.runners_distroless.image || 'N/A'}`);
+81 -1
View File
@@ -131,6 +131,9 @@ jobs:
outputs:
image_ref: ${{ steps.determine-tags.outputs.n8n_primary_tag }}
primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.n8n_primary_tag }}
n8n_pc_primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.n8n_pc_primary_tag }}
n8n_pc_sha_manifest_tag: ${{ steps.determine-tags.outputs.n8n_pc_sha_primary_tag }}
n8n_pc_date_manifest_tag: ${{ steps.determine-tags.outputs.n8n_pc_date_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 }}
@@ -206,6 +209,26 @@ jobs:
push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }}
tags: ${{ steps.determine-tags.outputs.n8n_tags }}
- name: Build and push n8n -pc Docker image
id: build-n8n-pc
if: needs.determine-build-context.outputs.push_enabled == 'true'
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
with:
context: .
file: ./docker/images/n8n/Dockerfile
build-args: |
NODE_VERSION=${{ env.NODE_VERSION }}
N8N_VERSION=${{ needs.determine-build-context.outputs.n8n_version }}
N8N_RELEASE_TYPE=${{ needs.determine-build-context.outputs.release_type }}
BUILDER_IMAGE=n8nio/node-pc:26.5.1-dev@sha256:9ecfe9d5c7ea835bccc604d06c3cb76a837b64496d4a999338cb8b53a50bb0bd
RUNTIME_IMAGE=n8nio/node-pc:26.5.1@sha256:e535408e7815904c98f81fb0203f7f657047d7db3d4edc77de44d2ea72c6bf18
IMAGE_DESCRIPTION=Workflow Automation Tool (pointer-compressed variant, internal to n8n Cloud, no support or stability guarantees)
platforms: ${{ matrix.docker_platform }}
provenance: false # Disabled - using SLSA L3 generator for isolated provenance
sbom: true
push: ${{ needs.determine-build-context.outputs.push_enabled == 'true' }}
tags: ${{ steps.determine-tags.outputs.n8n_pc_tags }}
- name: Kafka native binding smoke check
# `load: true` isn't an option here: with sbom:true, buildx produces a manifest
# list even for a single platform, and the docker exporter (what --load uses)
@@ -264,6 +287,8 @@ jobs:
outputs:
n8n_digest: ${{ steps.get-digests.outputs.n8n_digest }}
n8n_image: ${{ steps.get-digests.outputs.n8n_image }}
n8n_pc_digest: ${{ steps.get-digests.outputs.n8n_pc_digest }}
n8n_pc_image: ${{ steps.get-digests.outputs.n8n_pc_image }}
runners_digest: ${{ steps.get-digests.outputs.runners_digest }}
runners_image: ${{ steps.get-digests.outputs.runners_image }}
runners_distroless_digest: ${{ steps.get-digests.outputs.runners_distroless_digest }}
@@ -286,6 +311,7 @@ jobs:
- name: Create GHCR multi-arch manifests
env:
N8N_DATE_MANIFEST_TAG: ${{ needs.build-and-push-docker.outputs.n8n_date_manifest_tag }}
N8N_PC_DATE_MANIFEST_TAG: ${{ needs.build-and-push-docker.outputs.n8n_pc_date_manifest_tag }}
RUNNERS_DATE_MANIFEST_TAG: ${{ needs.build-and-push-docker.outputs.runners_date_manifest_tag }}
RUNNERS_DISTROLESS_DATE_MANIFEST_TAG: ${{ needs.build-and-push-docker.outputs.runners_distroless_date_manifest_tag }}
run: |
@@ -318,11 +344,13 @@ jobs:
# Create manifests for all images
create_manifest "n8n" "${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}"
create_manifest "n8n-pc" "${{ needs.build-and-push-docker.outputs.n8n_pc_primary_ghcr_manifest_tag }}"
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 "n8n-pc (sha)" "${{ needs.build-and-push-docker.outputs.n8n_pc_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 }}"
@@ -330,6 +358,7 @@ jobs:
# date_tag input was set (e.g. nightly), and create_manifest skips empty tags —
# so these calls are no-ops on non-dated builds.
create_manifest "n8n (date)" "$N8N_DATE_MANIFEST_TAG"
create_manifest "n8n-pc (date)" "$N8N_PC_DATE_MANIFEST_TAG"
create_manifest "runners (date)" "$RUNNERS_DATE_MANIFEST_TAG"
create_manifest "runners-distroless (date)" "$RUNNERS_DISTROLESS_DATE_MANIFEST_TAG"
@@ -346,13 +375,15 @@ jobs:
# Create manifests for each image type
declare -A images=(
["n8n"]="${VERSION}"
["n8n-pc"]="${VERSION}-pc"
["runners"]="${VERSION}"
["runners-distroless"]="${VERSION}-distroless"
)
for image in "${!images[@]}"; do
TAG_SUFFIX="${images[$image]}"
IMAGE_NAME="${image//-distroless/}" # Remove -distroless from image name
IMAGE_NAME="${image//-distroless/}" # Remove the variant suffix from the image name
IMAGE_NAME="${IMAGE_NAME//-pc/}"
echo "Creating Docker Hub manifest for $image"
docker buildx imagetools create \
@@ -365,6 +396,8 @@ jobs:
# to match docker-tags.mjs format: nightly-abc1234-distroless (not nightly-distroless-abc1234)
if [[ "$image" == *"-distroless"* ]]; then
SHA_SUFFIX="${VERSION}-${SHORT_SHA}-distroless"
elif [[ "$image" == *"-pc"* ]]; then
SHA_SUFFIX="${VERSION}-${SHORT_SHA}-pc"
else
SHA_SUFFIX="${TAG_SUFFIX}-${SHORT_SHA}"
fi
@@ -379,6 +412,8 @@ jobs:
if [[ -n "$DATE_TAG" ]]; then
if [[ "$image" == *"-distroless"* ]]; then
DATE_SUFFIX="${VERSION}-${DATE_TAG}-distroless"
elif [[ "$image" == *"-pc"* ]]; then
DATE_SUFFIX="${VERSION}-${DATE_TAG}-pc"
else
DATE_SUFFIX="${TAG_SUFFIX}-${DATE_TAG}"
fi
@@ -394,6 +429,7 @@ jobs:
id: get-digests
env:
N8N_TAG: ${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}
N8N_PC_TAG: ${{ needs.build-and-push-docker.outputs.n8n_pc_primary_ghcr_manifest_tag }}
RUNNERS_TAG: ${{ needs.build-and-push-docker.outputs.runners_primary_ghcr_manifest_tag }}
DISTROLESS_TAG: ${{ needs.build-and-push-docker.outputs.runners_distroless_primary_ghcr_manifest_tag }}
run: node .github/scripts/docker/get-manifest-digests.mjs
@@ -432,6 +468,25 @@ jobs:
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}
provenance-n8n-pc:
name: SLSA Provenance (n8n-pc)
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
if: |
needs.create_multi_arch_manifest.result == 'success' &&
needs.create_multi_arch_manifest.outputs.n8n_pc_digest != ''
permissions:
id-token: write
packages: write
actions: read
# SLSA L3 Provenance - Must use version tags (@vX.Y.Z), NOT SHAs
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ${{ needs.create_multi_arch_manifest.outputs.n8n_pc_image }}
digest: ${{ needs.create_multi_arch_manifest.outputs.n8n_pc_digest }}
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}
provenance-runners:
name: SLSA Provenance (runners)
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
@@ -479,6 +534,7 @@ jobs:
build-and-push-docker,
create_multi_arch_manifest,
provenance-n8n,
provenance-n8n-pc,
provenance-runners,
provenance-runners-distroless,
]
@@ -514,6 +570,14 @@ jobs:
--predicate security/vex.openvex.json \
${{ needs.create_multi_arch_manifest.outputs.n8n_image }}@${{ needs.create_multi_arch_manifest.outputs.n8n_digest }}
- name: Attest VEX to n8n-pc image
if: needs.create_multi_arch_manifest.outputs.n8n_pc_digest != ''
run: |
cosign attest --yes \
--type openvex \
--predicate security/vex.openvex.json \
${{ needs.create_multi_arch_manifest.outputs.n8n_pc_image }}@${{ needs.create_multi_arch_manifest.outputs.n8n_pc_digest }}
- name: Attest VEX to runners image
if: needs.create_multi_arch_manifest.outputs.runners_digest != ''
run: |
@@ -544,6 +608,7 @@ jobs:
build-and-push-docker,
create_multi_arch_manifest,
provenance-n8n,
provenance-n8n-pc,
provenance-runners,
provenance-runners-distroless,
]
@@ -583,6 +648,8 @@ jobs:
env:
N8N_IMAGE: ${{ needs.create_multi_arch_manifest.outputs.n8n_image }}
N8N_DIGEST: ${{ needs.create_multi_arch_manifest.outputs.n8n_digest }}
N8N_PC_IMAGE: ${{ needs.create_multi_arch_manifest.outputs.n8n_pc_image }}
N8N_PC_DIGEST: ${{ needs.create_multi_arch_manifest.outputs.n8n_pc_digest }}
RUNNERS_IMAGE: ${{ needs.create_multi_arch_manifest.outputs.runners_image }}
RUNNERS_DIGEST: ${{ needs.create_multi_arch_manifest.outputs.runners_digest }}
DISTROLESS_IMAGE: ${{ needs.create_multi_arch_manifest.outputs.runners_distroless_image }}
@@ -602,6 +669,19 @@ jobs:
image_ref: ${{ needs.build-and-push-docker.outputs.image_ref }}
secrets: inherit
security-scan-pc:
name: Security Scan (n8n-pc)
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]
if: |
success() &&
(needs.determine-build-context.outputs.release_type == 'stable' ||
needs.determine-build-context.outputs.release_type == 'nightly' ||
needs.determine-build-context.outputs.release_type == 'rc')
uses: ./.github/workflows/security-trivy-scan-callable.yml
with:
image_ref: ${{ needs.build-and-push-docker.outputs.n8n_pc_primary_ghcr_manifest_tag }}
secrets: inherit
security-scan-runners:
name: Security Scan (runners)
needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest]