diff --git a/.github/actions/build-n8n-docker/action.yml b/.github/actions/build-n8n-docker/action.yml index 4843cf869d5..37cf04a8d95 100644 --- a/.github/actions/build-n8n-docker/action.yml +++ b/.github/actions/build-n8n-docker/action.yml @@ -12,7 +12,7 @@ description: 'Builds n8nio/n8n + n8nio/runners test images and publishes them as inputs: build-variant: - description: 'standard or coverage. Coverage uses build:docker:coverage.' + description: 'standard, coverage, or pc. Coverage uses build:docker:coverage. pc builds n8n on the pointer-compressed Node base images.' required: false default: 'standard' @@ -35,6 +35,20 @@ runs: enable-docker-cache: true env: INCLUDE_TEST_CONTROLLER: 'true' + # Keep the pc pins in sync with the -pc build in docker-build-push.yml. + BUILDER_IMAGE: ${{ inputs.build-variant == 'pc' && 'n8nio/node-pc:26.7.0-dev@sha256:c64642dcb9464e50bd08aef8504affafeb795d01aed2f7ab158976c334a36ef6' || '' }} + RUNTIME_IMAGE: ${{ inputs.build-variant == 'pc' && 'n8nio/node-pc:26.7.0@sha256:6577d0742ad043baaa39db7dda2ce8aeb73f32c84eb241c49bf716f170c53297' || '' }} + + - name: Assert pointer compression + if: steps.cache-check.outputs.cache-hit != 'true' && inputs.build-variant == 'pc' + shell: bash + run: | + docker run --rm --entrypoint node n8nio/n8n:local -e " + if (!process.config.variables.v8_enable_pointer_compression) { + throw new Error('pc variant image is not pointer-compressed'); + } + console.log(process.version, 'pointer compression: on'); + " - name: Save image tarball if: steps.cache-check.outputs.cache-hit != 'true' diff --git a/.github/actions/load-n8n-docker/action.yml b/.github/actions/load-n8n-docker/action.yml index 81f17567ca5..1534fd00ed6 100644 --- a/.github/actions/load-n8n-docker/action.yml +++ b/.github/actions/load-n8n-docker/action.yml @@ -15,7 +15,7 @@ description: 'Restores the zstd-compressed n8n + runners image tarball from the inputs: build-variant: - description: 'Variant of the image to load. Must match what build-n8n-docker used (standard or coverage).' + description: 'Variant of the image to load. Must match what build-n8n-docker used (standard, coverage, or pc).' required: false default: 'standard' cache-sha: diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index ff872e9a86c..b75e6699883 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -220,8 +220,8 @@ jobs: 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 + BUILDER_IMAGE=n8nio/node-pc:26.7.0-dev@sha256:c64642dcb9464e50bd08aef8504affafeb795d01aed2f7ab158976c334a36ef6 + RUNTIME_IMAGE=n8nio/node-pc:26.7.0@sha256:6577d0742ad043baaa39db7dda2ce8aeb73f32c84eb241c49bf716f170c53297 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 diff --git a/.github/workflows/prepare-docker-reusable.yml b/.github/workflows/prepare-docker-reusable.yml index 110a24f173d..f0ecc05718d 100644 --- a/.github/workflows/prepare-docker-reusable.yml +++ b/.github/workflows/prepare-docker-reusable.yml @@ -11,7 +11,7 @@ on: workflow_call: inputs: build-variant: - description: 'standard or coverage' + description: 'standard, coverage, or pc' required: false default: 'standard' type: string diff --git a/.github/workflows/test-e2e-pc-nightly.yml b/.github/workflows/test-e2e-pc-nightly.yml index 7276adbe6b9..f36ebd0471f 100644 --- a/.github/workflows/test-e2e-pc-nightly.yml +++ b/.github/workflows/test-e2e-pc-nightly.yml @@ -1,70 +1,106 @@ name: 'Test: E2E on -pc image' -# Boots the pointer-compressed n8n image and runs the e2e suite against it. +# The e2e suite needs the test-only e2e controller, which published images +# exclude. So: assert the published -pc image boots pointer-compressed, then +# run the full suite against a test-enabled twin (build-variant: pc). on: + schedule: + - cron: '0 2 * * *' # after the nightly Docker build at midnight workflow_dispatch: inputs: - image: - description: 'n8n image to test' + published-image: + description: 'Published -pc image to health-check' required: false default: 'n8nio/n8n:nightly-pc' type: string - runners-image: - description: 'Task runner image to boot beside it' - required: false - default: 'n8nio/runners:nightly' - type: string permissions: contents: read -env: - TEST_IMAGE_N8N: ${{ inputs.image || 'n8nio/n8n:nightly-pc' }} - TEST_IMAGE_TASK_RUNNER: ${{ inputs.runners-image || 'n8nio/runners:nightly' }} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - e2e-pc: - name: E2E on -pc image (${{ matrix.shard }}/4) - runs-on: blacksmith-4vcpu-ubuntu-2204 - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - shard: [1, 2, 3, 4] + assert-published: + name: Published -pc image health check + runs-on: blacksmith-2vcpu-ubuntu-2204 + timeout-minutes: 10 steps: - - uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1 - with: - persist-credentials: false - - - name: Pull image and assert pointer compression + - name: Assert pointer compression and boot to healthz + env: + PUBLISHED_IMAGE: ${{ inputs.published-image || 'n8nio/n8n:nightly-pc' }} run: | - docker pull --quiet "$TEST_IMAGE_N8N" - docker run --rm --entrypoint node "$TEST_IMAGE_N8N" -e " + docker pull --quiet "$PUBLISHED_IMAGE" + docker run --rm --entrypoint node "$PUBLISHED_IMAGE" -e " if (!process.config.variables.v8_enable_pointer_compression) { throw new Error('image is not pointer-compressed'); } console.log(process.version, 'pointer compression: on'); " + docker run -d --name published-pc -p 5678:5678 "$PUBLISHED_IMAGE" + for i in $(seq 1 24); do + if curl -sf http://localhost:5678/healthz > /dev/null; then + echo "healthz reachable after ~$((i * 5))s" + exit 0 + fi + sleep 5 + done + echo '::error::published image did not reach /healthz within 120s' + docker logs published-pc 2>&1 | tail -50 + exit 1 - - name: Build the test harness + prepare-docker: + name: Prepare Docker (pc) + uses: ./.github/workflows/prepare-docker-reusable.yml + with: + build-variant: pc + runner: blacksmith-8vcpu-ubuntu-2204 + secrets: inherit + + generate-matrix: + name: Generate shard matrix + runs-on: blacksmith-2vcpu-ubuntu-2204 + outputs: + matrix: ${{ steps.gen.outputs.matrix }} + steps: + - uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1 + with: + persist-credentials: false + + - name: Setup uses: ./.github/actions/setup-nodejs with: - build-command: 'pnpm turbo build --filter=n8n-playwright...' + build-command: pnpm turbo run build --filter=@n8n/playwright-janitor - - name: Install Playwright browsers - working-directory: packages/testing/playwright - run: npx playwright install chromium --with-deps + - name: Generate matrix (16 shards, duration-weighted) + id: gen + run: | + MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 16 --orchestrate) + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" - - name: Run e2e against the image - working-directory: packages/testing/playwright - run: pnpm test:container:sqlite:e2e -- --shard=${{ matrix.shard }}/4 + e2e: + name: E2E on the pc twin + needs: [prepare-docker, generate-matrix] + uses: ./.github/workflows/test-e2e-reusable.yml + with: + test-mode: docker-artifact + test-command: pnpm --filter=n8n-playwright test:container:sqlite:e2e + workers: '1' + runner: blacksmith-4vcpu-ubuntu-2204 + timeout-minutes: 55 + pre-generated-matrix: ${{ needs.generate-matrix.outputs.matrix }} + artifact-prefix: pc + build-variant: pc + currents-project-id: 'LRxcNt' + secrets: inherit notify-on-failure: name: Notify Cats on -pc e2e failure runs-on: ubuntu-latest - needs: [e2e-pc] - if: always() && needs.e2e-pc.result == 'failure' && github.event_name == 'schedule' + needs: [assert-published, prepare-docker, generate-matrix, e2e] + if: always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/.github/workflows/test-e2e-reusable.yml b/.github/workflows/test-e2e-reusable.yml index c3af91ba6b3..12ab3bbb21b 100644 --- a/.github/workflows/test-e2e-reusable.yml +++ b/.github/workflows/test-e2e-reusable.yml @@ -53,7 +53,7 @@ on: default: 'e2e' type: string build-variant: - description: 'Build variant for docker-artifact mode. Must match the variant used by prepare-docker (standard or coverage).' + description: 'Build variant for docker-artifact mode. Must match the variant used by prepare-docker (standard, coverage, or pc).' required: false default: 'standard' type: string diff --git a/scripts/dockerize-n8n.mjs b/scripts/dockerize-n8n.mjs index ea4e25a8bc6..0b0537ee826 100755 --- a/scripts/dockerize-n8n.mjs +++ b/scripts/dockerize-n8n.mjs @@ -144,6 +144,11 @@ const noCache = process.env.DOCKER_BUILD_NO_CACHE === 'true'; const withBaseImage = process.env.DOCKER_BUILD_BASE_IMAGE === 'true'; // Opt-in: only cloud deploys the distroless runners image, so local builds skip it. const withDistroless = process.env.DOCKER_BUILD_DISTROLESS === 'true'; +// The pc CI variant pins the n8n base images; runners have no such variant. +const baseImageArgs = [ + process.env.BUILDER_IMAGE && `BUILDER_IMAGE=${process.env.BUILDER_IMAGE}`, + process.env.RUNTIME_IMAGE && `RUNTIME_IMAGE=${process.env.RUNTIME_IMAGE}`, +].filter(Boolean); // Keep in sync with NODE_VERSION in .github/workflows/docker-build-push.yml, // which is what the published images are actually built with. const nodeVersion = process.env.NODE_VERSION || '26.7.0'; @@ -196,6 +201,8 @@ async function main() { echo(`INFO: Platform: ${platform}`); if (noCache) echo(chalk.yellow('INFO: Docker layer cache disabled (DOCKER_BUILD_NO_CACHE=true)')); if (withBaseImage) echo(chalk.yellow('INFO: Building base image first (DOCKER_BUILD_BASE_IMAGE=true)')); + if (baseImageArgs.length > 0) + echo(chalk.yellow(`INFO: Base image overrides: ${baseImageArgs.join(', ')}`)); echo(chalk.gray('-'.repeat(47))); await checkPrerequisites(); @@ -215,7 +222,7 @@ async function main() { name: 'n8n', dockerfilePath: config.n8n.dockerfilePath, fullImageName: config.n8n.fullImageName, - buildArgs: nodeVersionArgs, + buildArgs: [...nodeVersionArgs, ...baseImageArgs], }); const runnersBuildTime = await buildDockerImage({