mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
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:
parent
7a47fddd90
commit
a31d003c05
51
.github/actions/build-n8n-docker/action.yml
vendored
Normal file
51
.github/actions/build-n8n-docker/action.yml
vendored
Normal 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
|
||||
23
.github/actions/load-n8n-docker/action.yml
vendored
Normal file
23
.github/actions/load-n8n-docker/action.yml
vendored
Normal 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
|
||||
38
.github/workflows/ci-pull-requests.yml
vendored
38
.github/workflows/ci-pull-requests.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
49
.github/workflows/prepare-docker-reusable.yml
vendored
Normal file
49
.github/workflows/prepare-docker-reusable.yml
vendored
Normal 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 }}
|
||||
137
.github/workflows/test-e2e-ci-reusable.yml
vendored
137
.github/workflows/test-e2e-ci-reusable.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
68
.github/workflows/test-e2e-reusable.yml
vendored
68
.github/workflows/test-e2e-reusable.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ test.use({
|
|||
});
|
||||
|
||||
test.describe(
|
||||
'OIDC Authentication @capability:oidc',
|
||||
'OIDC Authentication @capability:oidc @licensed',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Identity & Access' }],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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' }],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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' }],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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' }],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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' }],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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' }],
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user