ci: Centralize CI Docker image build and distribution (#28798)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Declan Carroll 2026-04-23 08:18:52 +01:00 committed by GitHub
parent 7a47fddd90
commit a31d003c05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 275 additions and 178 deletions

View File

@ -0,0 +1,51 @@
# Builds the n8n and runners CI test images (n8nio/n8n:local +
# n8nio/runners:local) and caches the tarball under a SHA-derived key so
# downstream jobs in the same workflow run can restore it via
# load-n8n-docker.
#
# Cache-aware: if the tarball for this SHA is already cached (e.g. a parent
# workflow ran prepare-docker earlier), the build step is skipped and this
# action becomes a no-op.
name: 'Build n8n CI Docker image'
description: 'Builds n8nio/n8n + n8nio/runners test images and publishes them as a SHA-keyed cache tarball for downstream shards.'
inputs:
build-variant:
description: 'standard or coverage. Coverage uses build:docker:coverage.'
required: false
default: 'standard'
runs:
using: 'composite'
steps:
- name: Check cache for existing image
id: cache-check
uses: actions/cache/restore@640a1c2554105b57832a23eea0b4672fc7a790d5 # v4.2.3
with:
key: n8n-docker-image-${{ github.sha }}
path: /tmp/n8n-image.tar.zst
lookup-only: true
- name: Build Docker image
if: steps.cache-check.outputs.cache-hit != 'true'
uses: ./.github/actions/setup-nodejs
with:
build-command: ${{ inputs.build-variant == 'coverage' && 'pnpm build:docker:coverage' || 'pnpm build:docker' }}
enable-docker-cache: true
env:
INCLUDE_TEST_CONTROLLER: 'true'
- name: Save image tarball
if: steps.cache-check.outputs.cache-hit != 'true'
shell: bash
run: |
docker save n8nio/n8n:local n8nio/runners:local | zstd -T0 -3 -o /tmp/n8n-image.tar.zst
ls -lh /tmp/n8n-image.tar.zst
- name: Publish image tarball to cache
if: steps.cache-check.outputs.cache-hit != 'true'
uses: actions/cache/save@640a1c2554105b57832a23eea0b4672fc7a790d5 # v4.2.3
with:
key: n8n-docker-image-${{ github.sha }}
path: /tmp/n8n-image.tar.zst

View File

@ -0,0 +1,23 @@
# Restores the n8n + runners image tarball (produced by build-n8n-docker
# under the SHA-derived cache key) and loads both images into the local
# docker daemon.
#
# After this action runs, `n8nio/n8n:local` and `n8nio/runners:local` are
# present on the runner.
name: 'Load n8n Docker images from cache'
description: 'Restores the zstd-compressed n8n + runners image tarball from the SHA-keyed GHA cache and loads both images into the local docker daemon.'
runs:
using: 'composite'
steps:
- name: Restore image tarball from cache
uses: actions/cache/restore@640a1c2554105b57832a23eea0b4672fc7a790d5 # v4.2.3
with:
key: n8n-docker-image-${{ github.sha }}
path: /tmp/n8n-image.tar.zst
fail-on-cache-miss: true
- name: Load n8n and runners images into docker
shell: bash
run: zstd -d -c /tmp/n8n-image.tar.zst | docker load

View File

@ -56,7 +56,9 @@ jobs:
!.github/**
e2e:
.github/workflows/test-e2e-*.yml
.github/scripts/cleanup-ghcr-images.mjs
.github/workflows/prepare-docker-reusable.yml
.github/actions/build-n8n-docker/**
.github/actions/load-n8n-docker/**
packages/testing/playwright/**
packages/testing/containers/**
workflows: .github/**
@ -149,10 +151,31 @@ jobs:
run: |
pnpm -r pack --dry-run
# Seeds the SHA-keyed Docker image cache once so that downstream e2e jobs
# (each of which invokes prepare-docker internally) short-circuit to a
# cache hit instead of racing to rebuild.
prepare-docker:
name: Prepare Docker
needs: install-and-build
if: >-
github.repository == 'n8n-io/n8n' &&
github.event_name != 'merge_group' &&
(needs.install-and-build.outputs.ci == 'true'
|| needs.install-and-build.outputs.e2e == 'true'
|| needs.install-and-build.outputs.e2e_performance == 'true')
uses: ./.github/workflows/prepare-docker-reusable.yml
with:
branch: ${{ needs.install-and-build.outputs.commit_sha }}
secrets: inherit
e2e-tests:
name: E2E Tests
needs: install-and-build
if: (needs.install-and-build.outputs.ci == 'true' || needs.install-and-build.outputs.e2e == 'true') && github.repository == 'n8n-io/n8n' && github.event_name != 'merge_group'
needs: [install-and-build, prepare-docker]
if: >-
needs.prepare-docker.result == 'success' &&
(needs.install-and-build.outputs.ci == 'true' || needs.install-and-build.outputs.e2e == 'true') &&
github.repository == 'n8n-io/n8n' &&
github.event_name != 'merge_group'
uses: ./.github/workflows/test-e2e-ci-reusable.yml
with:
branch: ${{ needs.install-and-build.outputs.commit_sha }}
@ -177,11 +200,12 @@ jobs:
e2e-performance:
name: E2E Performance
needs: install-and-build
needs: [install-and-build, prepare-docker]
# Performance is internal-only (license secrets required, not available on forks).
if: >-
(needs.install-and-build.outputs.ci == 'true' || needs.install-and-build.outputs.e2e_performance == 'true') &&
github.event_name == 'pull_request' &&
github.repository == 'n8n-io/n8n'
needs.prepare-docker.result == 'success' &&
needs.install-and-build.outputs.e2e_performance == 'true' &&
github.event.pull_request.head.repo.fork != true
uses: ./.github/workflows/test-e2e-performance-reusable.yml
secrets: inherit

View File

@ -0,0 +1,49 @@
name: 'Prepare n8n Docker (reusable)'
# Reusable workflow that ensures the n8n + runners CI test images for the
# current commit SHA are present in the GHA cache. Cache-aware: if another
# job in the same run already populated `n8n-docker-image-<sha>`, this
# becomes a no-op.
#
# Downstream jobs restore the same SHA-keyed cache via load-n8n-docker.
on:
workflow_call:
inputs:
build-variant:
description: 'standard or coverage'
required: false
default: 'standard'
type: string
runner:
description: 'Runner for the build.'
required: false
default: 'blacksmith-4vcpu-ubuntu-2204'
type: string
branch:
description: 'Git ref to check out.'
required: false
default: ''
type: string
jobs:
prepare:
name: 'Build & publish image'
runs-on: ${{ inputs.runner }}
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Build and publish image
uses: ./.github/actions/build-n8n-docker
with:
build-variant: ${{ inputs.build-variant }}
env:
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}

View File

@ -36,17 +36,22 @@ on:
type: string
default: ''
env:
DOCKER_IMAGE: ghcr.io/${{ github.repository }}:ci-${{ github.run_id }}
# Fork PRs route to the community path (sqlite-only, no @licensed tests) because
# they have no license-secret access. Non-PR events (schedule, manual dispatch)
# always use the internal path.
jobs:
prepare:
name: 'Prepare E2E'
if: ${{ !github.event.pull_request.head.repo.fork }}
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
packages: write
contents: read
# Ensures the n8n + runners image tarball is cached under the SHA-derived
# key. Cache-aware: no-op if an ancestor workflow already populated it.
prepare-docker:
uses: ./.github/workflows/prepare-docker-reusable.yml
with:
branch: ${{ inputs.branch }}
secrets: inherit
prepare-matrix:
name: 'Prepare Matrix'
runs-on: blacksmith-2vcpu-ubuntu-2204
outputs:
matrix: ${{ steps.generate-matrix.outputs.matrix }}
skip-tests: ${{ steps.generate-matrix.outputs.skip-tests }}
@ -57,26 +62,12 @@ jobs:
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Login to GHCR
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push to GHCR
- name: Setup Environment
uses: ./.github/actions/setup-nodejs
with:
build-command: 'pnpm build:docker'
enable-docker-cache: true
env:
INCLUDE_TEST_CONTROLLER: 'true'
IMAGE_BASE_NAME: ghcr.io/${{ github.repository }}
IMAGE_TAG: ci-${{ github.run_id }}
RUNNERS_IMAGE_BASE_NAME: ghcr.io/${{ github.repository_owner }}/runners
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
# Matrix generation calls the janitor CLI (via distribute-tests.mjs),
# which needs packages/testing/janitor/dist/cli.js on disk.
build-command: 'pnpm turbo run build --filter=@n8n/playwright-janitor'
- name: Get changed files for impact analysis
if: ${{ inputs.playwright-only }}
@ -98,79 +89,49 @@ jobs:
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
echo "skip-tests=$(node -e "process.stdout.write(JSON.parse(process.argv[1])[0]?.skip === true ? 'true' : 'false')" "$MATRIX")" >> "$GITHUB_OUTPUT"
# Internal-only 1-spec fail-fast sanity check on sqlite.
sqlite-sanity:
needs: [prepare]
name: 'SQLite: Sanity Check'
if: ${{ !github.event.pull_request.head.repo.fork }}
needs: [prepare-docker]
if: ${{ github.event.pull_request.head.repo.fork != true }}
uses: ./.github/workflows/test-e2e-reusable.yml
with:
branch: ${{ inputs.branch }}
test-mode: docker-pull
docker-image: ghcr.io/${{ github.repository }}:ci-${{ github.run_id }}
test-mode: docker-artifact
test-command: pnpm --filter=n8n-playwright test:container:sqlite:e2e tests/e2e/building-blocks/workflow-entry-points.spec.ts
shards: 1
runner: blacksmith-2vcpu-ubuntu-2204
workers: '1'
pre-generated-matrix: '[{"shard":1,"images":""}]'
n8n-env: ${{ inputs.n8n-env }}
# Multi-main: postgres + redis + caddy + 2 mains + 1 worker
# Only runs for internal PRs (not community/fork PRs)
# Pulls pre-built Docker image from GHCR
multi-main-e2e:
needs: [prepare]
name: 'Multi-Main: E2E'
if: ${{ !github.event.pull_request.head.repo.fork && needs.prepare.outputs.skip-tests != 'true' }}
uses: ./.github/workflows/test-e2e-reusable.yml
with:
branch: ${{ inputs.branch }}
test-mode: docker-pull
docker-image: ghcr.io/${{ github.repository }}:ci-${{ github.run_id }}
test-command: pnpm --filter=n8n-playwright test:container:multi-main:e2e
shards: 16
runner: blacksmith-2vcpu-ubuntu-2204
workers: '1'
use-custom-orchestration: true
pre-generated-matrix: ${{ needs.prepare.outputs.matrix }}
n8n-env: ${{ inputs.n8n-env }}
secrets: inherit
# Community PR tests: Local mode with SQLite (no container building, no secrets required)
# Runs on GitHub-hosted runners without Currents reporting
community-e2e:
name: 'Community: E2E'
if: ${{ github.event.pull_request.head.repo.fork }}
# Internal-only full run: postgres + redis + caddy + 2 mains + 1 worker.
# Requires enterprise license secrets so cannot run in community mode.
multi-main-e2e:
needs: [prepare-docker, prepare-matrix]
name: 'Multi-Main: E2E'
if: ${{ github.event.pull_request.head.repo.fork != true && needs.prepare-matrix.outputs.skip-tests != 'true' }}
uses: ./.github/workflows/test-e2e-reusable.yml
with:
branch: ${{ inputs.branch }}
test-mode: local
test-command: pnpm --filter=n8n-playwright test:local
shards: 7
runner: ubuntu-latest
test-mode: docker-artifact
test-command: pnpm --filter=n8n-playwright test:container:multi-main:e2e
workers: '1'
pre-generated-matrix: ${{ needs.prepare-matrix.outputs.matrix }}
n8n-env: ${{ inputs.n8n-env }}
secrets: inherit
# Community-mode full run on sqlite (no enterprise license needed).
# Excludes @licensed tests since community contributors have no license secrets.
community-sqlite-e2e:
needs: [prepare-docker, prepare-matrix]
name: 'SQLite: E2E (community)'
if: ${{ github.event.pull_request.head.repo.fork == true && needs.prepare-matrix.outputs.skip-tests != 'true' }}
uses: ./.github/workflows/test-e2e-reusable.yml
with:
branch: ${{ inputs.branch }}
test-mode: docker-artifact
test-command: pnpm --filter=n8n-playwright test:container:sqlite:e2e --grep-invert="@licensed"
workers: '1'
pre-generated-matrix: ${{ needs.prepare-matrix.outputs.matrix }}
n8n-env: ${{ inputs.n8n-env }}
upload-failure-artifacts: true
# Cleanup ephemeral Docker image from GHCR after tests complete
# Local runner cleanup is handled by each test shard in test-e2e-reusable.yml
cleanup-ghcr:
name: 'Cleanup GHCR Image'
needs: [prepare, multi-main-e2e, sqlite-sanity]
if: ${{ !failure() && !cancelled() && !github.event.pull_request.head.repo.fork }}
runs-on: blacksmith-2vcpu-ubuntu-2204
permissions:
packages: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Delete images from GHCR
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GHCR_ORG: ${{ github.repository_owner }}
GHCR_REPO: ${{ github.event.repository.name }}
run: node .github/scripts/cleanup-ghcr-images.mjs --tag ci-${{ github.run_id }}
secrets: inherit

View File

@ -39,7 +39,7 @@ jobs:
--workers=${{ env.PLAYWRIGHT_WORKERS }}
env:
BUILD_WITH_COVERAGE: 'true'
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
CURRENTS_RECORD_KEY: ${{ vars.CURRENTS_RECORD_VAR }}
CURRENTS_PROJECT_ID: 'LRxcNt'
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}

View File

@ -28,11 +28,22 @@ on:
type: string
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.gen.outputs.matrix }}
steps:
- id: gen
run: |
MATRIX=$(seq 1 ${{ inputs.shards }} | awk 'BEGIN{printf "["} {printf "%s{\"shard\":%s,\"images\":\"\"}", (NR>1?",":""), $1} END{printf "]"}')
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
build-and-test:
needs: generate-matrix
uses: ./.github/workflows/test-e2e-reusable.yml
with:
test-mode: docker-pull
shards: ${{ inputs.shards }}
docker-image: ${{ inputs.image }}
test-command: pnpm --filter=n8n-playwright test:container:standard
pre-generated-matrix: ${{ needs.generate-matrix.outputs.matrix }}
secrets: inherit

View File

@ -17,7 +17,12 @@ concurrency:
cancel-in-progress: true
jobs:
prepare-docker:
uses: ./.github/workflows/prepare-docker-reusable.yml
secrets: inherit
benchmark:
needs: [prepare-docker]
name: ${{ matrix.profile }}
strategy:
fail-fast: false
@ -31,9 +36,8 @@ jobs:
runner: blacksmith-8vcpu-ubuntu-2204
uses: ./.github/workflows/test-e2e-reusable.yml
with:
test-mode: docker-build
test-mode: docker-artifact
test-command: pnpm --filter=n8n-playwright test:all --project='${{ matrix.profile }}:infrastructure' --workers=1
shards: 1
runner: ${{ matrix.runner }}
timeout-minutes: 60
secrets: inherit

View File

@ -7,11 +7,16 @@ on:
- cron: '0 0 * * *' # Runs daily at midnight
jobs:
# Cache-aware: no-op if an ancestor already populated the SHA-keyed cache.
prepare-docker:
uses: ./.github/workflows/prepare-docker-reusable.yml
secrets: inherit
build-and-test-performance:
needs: [prepare-docker]
uses: ./.github/workflows/test-e2e-reusable.yml
with:
test-mode: docker-build
test-mode: docker-artifact
test-command: pnpm --filter=n8n-playwright test:performance
shards: 1
currents-project-id: 'O9BJaN'
secrets: inherit

View File

@ -8,7 +8,7 @@ on:
required: false
type: string
test-mode:
description: 'Test mode: local (pnpm start from local), docker-build, or docker-pull'
description: 'Test mode: local, docker-artifact, or docker-pull'
required: false
default: 'local'
type: string
@ -17,13 +17,8 @@ on:
required: false
default: 'pnpm --filter=n8n-playwright test:local'
type: string
shards:
description: 'Number of parallel shards'
required: false
default: 8
type: number
docker-image:
description: 'Docker image to use (for docker-pull mode). The runners image is derived automatically from the n8n image.'
description: 'Docker image to use (docker-pull mode only). Ignored in docker-artifact mode (image is loaded as n8nio/n8n:local).'
required: false
default: 'n8nio/n8n:nightly'
type: string
@ -37,11 +32,6 @@ on:
required: false
default: 'blacksmith-2vcpu-ubuntu-2204'
type: string
use-custom-orchestration:
description: 'Use duration-based custom orchestration instead of Playwright sharding'
required: false
default: false
type: boolean
timeout-minutes:
description: 'Job timeout in minutes'
required: false
@ -58,9 +48,9 @@ on:
default: 'LRxcNt'
type: string
pre-generated-matrix:
description: 'Pre-generated shard matrix JSON (skips matrix job if provided)'
description: 'Pre-generated shard matrix JSON. Defaults to 1 shard; multi-shard callers pass their own (see test-e2e-ci-reusable.yml prepare-matrix).'
required: false
default: ''
default: '[{"shard":1,"images":""}]'
type: string
n8n-env:
description: 'JSON string of n8n env vars to inject into test containers, e.g. {"N8N_EXPRESSION_ENGINE":"vm"}'
@ -73,44 +63,22 @@ env:
PLAYWRIGHT_WORKERS: ${{ inputs.workers != '' && inputs.workers || '2' }}
# Browser cache location - must match install-browsers script
PLAYWRIGHT_BROWSERS_PATH: packages/testing/playwright/.playwright-browsers
TEST_IMAGE_N8N: ${{ inputs.test-mode == 'docker-build' && 'n8nio/n8n:local' || inputs.docker-image }}
# docker-artifact loads the image locally as n8nio/n8n:local.
TEST_IMAGE_N8N: ${{ inputs.test-mode == 'docker-artifact' && 'n8nio/n8n:local' || inputs.docker-image }}
N8N_SKIP_LICENSES: 'true'
CURRENTS_CI_BUILD_ID: ${{ github.repository }}-${{ github.run_id }}-${{ github.run_attempt }}
CURRENTS_PROJECT_ID: ${{ inputs.currents-project-id }}
jobs:
matrix:
if: ${{ inputs.pre-generated-matrix == '' }}
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-2vcpu-ubuntu-2204' }}
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Setup Environment
uses: ./.github/actions/setup-nodejs
with:
build-command: ''
- name: Generate shard matrix
id: generate
run: echo "matrix=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix ${{ inputs.shards }} ${{ inputs.use-custom-orchestration && '--orchestrate' || '' }})" >> "$GITHUB_OUTPUT"
test:
needs: matrix
if: ${{ !cancelled() }}
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || inputs.runner }}
timeout-minutes: ${{ inputs.timeout-minutes }}
permissions:
packages: read
contents: read
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(inputs.pre-generated-matrix || needs.matrix.outputs.matrix) }}
include: ${{ fromJSON(inputs.pre-generated-matrix) }}
name: Shard ${{ matrix.shard }}
steps:
@ -122,14 +90,7 @@ jobs:
- name: Setup Environment
uses: ./.github/actions/setup-nodejs
with:
# docker-build: build app + docker image locally
# docker-pull: no build needed, image is pre-built
# local: build app for local server
build-command: ${{ inputs.test-mode == 'docker-build' && 'pnpm build:docker' || 'pnpm build' }}
enable-docker-cache: ${{ inputs.test-mode == 'docker-build' }}
env:
INCLUDE_TEST_CONTROLLER: ${{ inputs.test-mode == 'docker-build' && 'true' || '' }}
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
@ -137,9 +98,9 @@ jobs:
- name: Install Browsers
run: pnpm turbo run install-browsers --filter=n8n-playwright
- name: Login to GHCR
if: ${{ inputs.test-mode == 'docker-pull' }}
uses: ./.github/actions/docker-registry-login
- name: Load n8n image from cache
if: ${{ inputs.test-mode == 'docker-artifact' }}
uses: ./.github/actions/load-n8n-docker
- name: Pre-pull Test Container Images
if: ${{ !contains(inputs.test-command, 'test:local') }}
@ -149,7 +110,7 @@ jobs:
# Uses pre-distributed specs if orchestration enabled, otherwise falls back to Playwright sharding
run: ${{ inputs.test-command }} --workers=${{ env.PLAYWRIGHT_WORKERS }} ${{ matrix.specs || format('--shard={0}/{1}', matrix.shard, strategy.job-total) }}
env:
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
CURRENTS_RECORD_KEY: ${{ vars.CURRENTS_RECORD_VAR }}
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
@ -168,13 +129,6 @@ jobs:
packages/testing/playwright/playwright-report/
retention-days: 7
- name: Cleanup cached CI images
if: ${{ inputs.test-mode == 'docker-pull' }}
continue-on-error: true
run: |
docker images --format '{{.Repository}}:{{.Tag}}' | grep -E 'ghcr\.io/n8n-io/(n8n|runners):(ci|pr)-' | xargs -r docker rmi || true
docker system prune -f || true
- name: Cancel Currents run if workflow is cancelled
if: ${{ cancelled() }}
env:

View File

@ -16,9 +16,6 @@ on:
ENCRYPTION_KEY:
description: 'Encryption key for n8n operations.'
required: true
CURRENTS_RECORD_KEY:
description: 'Currents record key for uploading test results.'
required: true
env:
NODE_OPTIONS: --max-old-space-size=3072
@ -44,12 +41,12 @@ jobs:
- name: Run Workflow Tests
run: pnpm --filter=n8n-playwright test:workflows --workers 4
env:
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
CURRENTS_RECORD_KEY: ${{ vars.CURRENTS_RECORD_VAR }}
CURRENTS_PROJECT_ID: 'mpLFH9'
- name: Run Workflow Schema Tests
if: ${{ inputs.compare_schemas == 'true' }}
run: pnpm --filter=n8n-playwright test:workflows:schema
env:
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
CURRENTS_RECORD_KEY: ${{ vars.CURRENTS_RECORD_VAR }}
CURRENTS_PROJECT_ID: 'mpLFH9'

View File

@ -25,7 +25,7 @@ export const postgres: Service<PostgresResult> = {
.withDatabase('n8n_db')
.withUsername('n8n_user')
.withPassword('test_password')
.withStartupTimeout(30000)
.withStartupTimeout(40000)
.withLabels({
'com.docker.compose.project': projectName,
'com.docker.compose.service': HOSTNAME,

View File

@ -9,12 +9,26 @@ export type ObservabilityWorkerFixtures = {
n8nContainer: N8NStack;
};
/**
* `stack.services` is a Proxy whose factory throws when a service isn't in the
* project config (e.g. sqlite:e2e has no observability services). Optional
* chaining doesn't help the throw happens inside the factory invocation.
*/
function tryGetObservability(stack: N8NStack | undefined) {
if (!stack) return undefined;
try {
return stack.services?.observability;
} catch {
return undefined;
}
}
async function attachLogsOnFailure(
stack: N8NStack,
testInfo: TestInfo,
options: { lookbackMinutes?: number } = {},
): Promise<void> {
const obs = stack.services?.observability;
const obs = tryGetObservability(stack);
if (!obs) return;
const lookback = options.lookbackMinutes ?? 5;
@ -62,7 +76,7 @@ async function attachLogsOnFailure(
}
async function attachMetricsOnFailure(stack: N8NStack, testInfo: TestInfo): Promise<void> {
const obs = stack.services?.observability;
const obs = tryGetObservability(stack);
if (!obs) return;
try {
@ -91,12 +105,16 @@ export const observabilityFixtures: Fixtures<
async ({ n8nContainer }, use, testInfo) => {
await use(undefined);
if (testInfo.status !== testInfo.expectedStatus && n8nContainer?.services?.observability) {
await Promise.all([
attachLogsOnFailure(n8nContainer, testInfo),
attachMetricsOnFailure(n8nContainer, testInfo),
]);
}
// n8nContainer is undefined when fixture setup failed (e.g. postgres timeout);
// observability may be unconfigured for this project (sqlite:e2e).
// Both cases must be handled gracefully so teardown never masks the real failure.
if (testInfo.status === testInfo.expectedStatus) return;
if (!tryGetObservability(n8nContainer)) return;
await Promise.all([
attachLogsOnFailure(n8nContainer, testInfo),
attachMetricsOnFailure(n8nContainer, testInfo),
]);
},
{ auto: true },
],

View File

@ -6,7 +6,7 @@ test.use({
});
test.describe(
'OIDC Authentication @capability:oidc',
'OIDC Authentication @capability:oidc @licensed',
{
annotation: [{ type: 'owner', description: 'Identity & Access' }],
},

View File

@ -27,7 +27,7 @@ test.use({
* Credential status returned (missing / configured)
*/
test.describe(
'Dynamic Credentials: execution-status @capability:dynamic-credentials',
'Dynamic Credentials: execution-status @capability:dynamic-credentials @licensed',
{
annotation: [{ type: 'owner', description: 'Identity & Access' }],
},

View File

@ -33,7 +33,7 @@ test.use({
* 10. Wait for execution and assert success (HTTP node resolved credential + called userinfo)
*/
test.describe(
'Dynamic Credentials: webhook execution @capability:dynamic-credentials',
'Dynamic Credentials: webhook execution @capability:dynamic-credentials @licensed',
{
annotation: [{ type: 'owner', description: 'Identity & Access' }],
},

View File

@ -19,7 +19,7 @@ test.use({
});
test.describe(
'Dynamic Credentials: resolver deletion cleanup @capability:dynamic-credentials',
'Dynamic Credentials: resolver deletion cleanup @capability:dynamic-credentials @licensed',
{
annotation: [{ type: 'owner', description: 'Identity & Access' }],
},

View File

@ -15,7 +15,7 @@ import { test, expect } from '../../../../fixtures/base';
test.use({ capability: 'observability' });
test.describe(
'Log Streaming to VictoriaLogs @capability:observability',
'Log Streaming to VictoriaLogs @capability:observability @licensed',
{
annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }],
},

View File

@ -12,7 +12,7 @@ import { test, expect } from '../../../../fixtures/base';
test.use({ capability: 'observability' });
test.describe(
'Log Streaming UI E2E @capability:observability',
'Log Streaming UI E2E @capability:observability @licensed',
{
annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }],
},