ci: Run DB tests against the full supported Postgres range (#35960)

This commit is contained in:
Iván Ovejero 2026-08-11 11:49:07 +02:00 committed by GitHub
parent acd4fdacc9
commit 4afbd16dc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 355 additions and 37 deletions

View File

@ -520,6 +520,7 @@ Scripts in `.github/scripts/`:
|-------------------------|-------------------|---------------------------|
| `validate-docs-links.js`| Check doc URLs | `util-check-docs-urls.yml`|
| `send-build-stats.mjs` | Build telemetry | `setup-nodejs` action |
| `db-test-matrix.mjs` | DB test matrix from `postgres-versions.json` | `ci-pull-requests.yml` |
### Slack Scripts

View File

@ -1,6 +1,6 @@
services:
postgres:
image: postgres:16
image: ${TEST_IMAGE_POSTGRES:?set it from packages/testing/containers/postgres-versions.json}
restart: always
environment:
- POSTGRES_DB=n8n
@ -8,5 +8,7 @@ services:
- POSTGRES_PASSWORD=password
ports:
- 5432:5432
# Not PGDATA: Postgres 18+ moved it under /var/lib/postgresql/<major>/ and
# refuses to start when /var/lib/postgresql/data is mounted.
tmpfs:
- /var/lib/postgresql/data
- /var/lib/postgresql

95
.github/scripts/db-test-matrix.mjs vendored Normal file
View File

@ -0,0 +1,95 @@
// Builds the job matrix for test-db-reusable.yml. Node builtins only: it runs in a
// job that installs the monorepo's dependencies, not this folder's.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
export const POSTGRES_VERSIONS_PATH = 'packages/testing/containers/postgres-versions.json';
/** The matrix wall is bounded by its slowest leg, so a larger runner buys nothing. */
const RUNNER = 'blacksmith-4vcpu-ubuntu-2204';
/**
* @typedef {Object} PostgresVersions
* @property {string} primary
* @property {Array<{ major: number, image: string, support?: string }>} matrix
*/
/** @returns {PostgresVersions} */
export function readPostgresVersions(repoRoot = REPO_ROOT) {
const file = path.join(repoRoot, POSTGRES_VERSIONS_PATH);
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
/**
* Coverage and the schema-docs check run on the primary Postgres leg only, since
* the committed docs come from that one version.
*
* @param {PostgresVersions} versions
*/
export function buildMatrix(versions) {
const { primary, matrix } = versions;
if (!Array.isArray(matrix) || matrix.length === 0) {
throw new Error(`${POSTGRES_VERSIONS_PATH}: "matrix" must be a non-empty array`);
}
// Checked first: everything below treats the last entry as the newest.
const majors = matrix.map((entry) => entry.major);
for (let i = 1; i < majors.length; i++) {
if (majors[i] <= majors[i - 1]) {
throw new Error(
`${POSTGRES_VERSIONS_PATH}: "matrix" must be sorted by ascending major, got ${majors.join(', ')}`,
);
}
}
const newest = matrix[matrix.length - 1];
if (newest.image !== primary) {
throw new Error(
`${POSTGRES_VERSIONS_PATH}: "primary" (${primary}) must be the newest "matrix" entry's image (${newest.image})`,
);
}
// An exact minor keeps CI reproducible across Postgres point releases.
for (const { major, image } of matrix) {
const pinned = /^postgres:(\d+)\.\d+-alpine$/.exec(image);
if (!pinned) {
throw new Error(
`${POSTGRES_VERSIONS_PATH}: "${image}" must pin an exact minor, e.g. postgres:${major}.4-alpine`,
);
}
if (Number(pinned[1]) !== major) {
throw new Error(`${POSTGRES_VERSIONS_PATH}: "${image}" does not match major ${major}`);
}
}
return [
{
name: 'SQLite Pooled',
runner: RUNNER,
'test-cmd': 'pnpm test:sqlite',
'migration-cmd': 'pnpm test:sqlite:migrations',
'schema-check-cmd': 'pnpm --filter=@n8n/db schema:check:sqlite',
TEST_IMAGE_POSTGRES: undefined,
collectCoverage: 'false',
},
...matrix.map(({ major, image }) => ({
name: `Postgres ${major}`,
runner: RUNNER,
'test-cmd': 'pnpm test:postgres:integration:tc',
'migration-cmd': 'pnpm test:postgres:migrations:tc',
'schema-check-cmd': image === primary ? 'pnpm --filter=@n8n/db schema:check:postgres' : '',
TEST_IMAGE_POSTGRES: image,
collectCoverage: image === primary ? 'true' : 'false',
})),
];
}
// Skipped when imported by the tests.
if (process.argv[1] === fileURLToPath(import.meta.url)) {
console.log(JSON.stringify(buildMatrix(readPostgresVersions())));
}

128
.github/scripts/db-test-matrix.test.mjs vendored Normal file
View File

@ -0,0 +1,128 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { buildMatrix, readPostgresVersions, POSTGRES_VERSIONS_PATH } from './db-test-matrix.mjs';
// node --test ./.github/scripts/db-test-matrix.test.mjs
const versions = () => ({
primary: 'postgres:18.4-alpine',
matrix: [
{ major: 16, support: 'compatibility', image: 'postgres:16.14-alpine' },
{ major: 17, support: 'supported', image: 'postgres:17.10-alpine' },
{ major: 18, support: 'supported', image: 'postgres:18.4-alpine' },
],
});
describe('postgres-versions.json', () => {
it('is valid, so the DB workflow can build a matrix from it', () => {
assert.doesNotThrow(() => buildMatrix(readPostgresVersions()));
});
it('covers the supported range plus one compatibility major', () => {
const { matrix } = readPostgresVersions();
const supported = matrix.filter((entry) => entry.support === 'supported');
const compatibility = matrix.filter((entry) => entry.support === 'compatibility');
assert.equal(supported.length, 2, `${POSTGRES_VERSIONS_PATH}: expected two supported majors`);
assert.equal(
compatibility.length,
1,
`${POSTGRES_VERSIONS_PATH}: expected one compatibility major`,
);
assert.ok(compatibility[0].major < Math.min(...supported.map((entry) => entry.major)));
});
});
describe('buildMatrix', () => {
it('emits one leg per database', () => {
const legs = buildMatrix(versions());
assert.deepEqual(
legs.map((leg) => leg.name),
['SQLite Pooled', 'Postgres 16', 'Postgres 17', 'Postgres 18'],
);
});
it('passes each Postgres leg its own pinned image', () => {
const legs = buildMatrix(versions());
assert.deepEqual(
legs.filter((leg) => leg.TEST_IMAGE_POSTGRES).map((leg) => leg.TEST_IMAGE_POSTGRES),
['postgres:16.14-alpine', 'postgres:17.10-alpine', 'postgres:18.4-alpine'],
);
assert.equal(legs[0].TEST_IMAGE_POSTGRES, undefined, 'SQLite leg needs no Postgres image');
});
it('runs every leg against both the integration and the migration suite', () => {
for (const leg of buildMatrix(versions())) {
assert.ok(leg['test-cmd'], `${leg.name} has no test command`);
assert.ok(leg['migration-cmd'], `${leg.name} has no migration command`);
}
});
it('collects coverage and checks schema docs on the primary Postgres leg only', () => {
const legs = buildMatrix(versions());
const collecting = legs.filter((leg) => leg.collectCoverage === 'true');
assert.deepEqual(
collecting.map((leg) => leg.name),
['Postgres 18'],
);
const checkingPostgresSchema = legs.filter(
(leg) => leg['schema-check-cmd'] === 'pnpm --filter=@n8n/db schema:check:postgres',
);
assert.deepEqual(
checkingPostgresSchema.map((leg) => leg.name),
['Postgres 18'],
);
const skipping = legs.filter((leg) => leg['schema-check-cmd'] === '');
assert.deepEqual(
skipping.map((leg) => leg.name),
['Postgres 16', 'Postgres 17'],
);
});
it('still checks the SQLite schema docs', () => {
const legs = buildMatrix(versions());
assert.equal(legs[0]['schema-check-cmd'], 'pnpm --filter=@n8n/db schema:check:sqlite');
});
it('rejects a primary that is not the newest major', () => {
const stale = { ...versions(), primary: 'postgres:17.10-alpine' };
assert.throws(() => buildMatrix(stale), /"primary".*must be the newest/);
});
it('rejects majors that are not in ascending order', () => {
const unsorted = versions();
[unsorted.matrix[0], unsorted.matrix[1]] = [unsorted.matrix[1], unsorted.matrix[0]];
assert.throws(() => buildMatrix(unsorted), /ascending major, got 17, 16, 18/);
});
it('rejects a floating image tag', () => {
const floating = {
primary: 'postgres:18-alpine',
matrix: [{ major: 18, image: 'postgres:18-alpine' }],
};
assert.throws(() => buildMatrix(floating), /must pin an exact minor/);
});
it('rejects an image whose major disagrees with its entry', () => {
const mismatched = {
primary: 'postgres:17.10-alpine',
matrix: [{ major: 18, image: 'postgres:17.10-alpine' }],
};
assert.throws(() => buildMatrix(mismatched), /does not match major 18/);
});
it('rejects an empty matrix', () => {
assert.throws(() => buildMatrix({ primary: 'postgres:18.4-alpine', matrix: [] }), /non-empty/);
});
});

View File

@ -33,6 +33,7 @@ jobs:
commit_sha: ${{ steps.commit-sha.outputs.sha }}
merge_base: ${{ steps.ci-filter.outputs.merge-base }}
matrix: ${{ steps.generate-matrix.outputs.matrix }}
db_test_matrix: ${{ steps.generate-db-test-matrix.outputs.db_test_matrix }}
skip_tests: ${{ steps.generate-matrix.outputs.skip-tests }}
affected_packages: ${{ steps.affected-packages.outputs.list }}
changed_files: ${{ steps.ci-filter.outputs.changed-files }}
@ -144,7 +145,10 @@ jobs:
packages/@n8n/scheduler/src/**
packages/cli/**/__tests__/**
packages/testing/containers/services/postgres.ts
packages/testing/containers/postgres-versions.json
packages/testing/containers/test-containers.ts
.github/workflows/test-db-reusable.yml
.github/scripts/db-test-matrix.mjs
docs/generated/**
.tbls.sqlite.yml
.tbls.postgres.yml
@ -175,6 +179,12 @@ 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"
# Generated here because a reusable workflow cannot resolve its own matrix.
- name: Generate DB test matrix
id: generate-db-test-matrix
if: fromJSON(steps.ci-filter.outputs.results).db
run: echo "db_test_matrix=$(node .github/scripts/db-test-matrix.mjs)" >> "$GITHUB_OUTPUT"
- name: Compute affected packages
id: affected-packages
if: fromJSON(steps.ci-filter.outputs.results).unit
@ -322,6 +332,7 @@ jobs:
uses: ./.github/workflows/test-db-reusable.yml
with:
ref: ${{ needs.install-and-build.outputs.commit_sha }}
matrix: ${{ needs.install-and-build.outputs.db_test_matrix }}
performance:
name: Performance

View File

@ -7,6 +7,10 @@ on:
required: false
type: string
default: ''
matrix:
description: 'Job matrix JSON, from .github/scripts/db-test-matrix.mjs.'
required: true
type: string
env:
NODE_OPTIONS: '--max-old-space-size=6144'
@ -19,24 +23,7 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- name: SQLite Pooled
runner: blacksmith-4vcpu-ubuntu-2204
test-cmd: pnpm test:sqlite
migration-cmd: pnpm test:sqlite:migrations
schema-check-cmd: pnpm --filter=@n8n/db schema:check:sqlite
collectCoverage: 'false'
- name: Postgres 16
# 4vcpu is enough since persistent-worker + template-DB make PG16
# complete in ~480s, which is now close to the SQLite leg (~360s)
# — the matrix wall is bounded by the slower leg, so the larger
# 8vcpu runner buys headroom we can't cash in anywhere.
runner: blacksmith-4vcpu-ubuntu-2204
test-cmd: pnpm test:postgres:integration:tc
migration-cmd: pnpm test:postgres:migrations:tc
schema-check-cmd: pnpm --filter=@n8n/db schema:check:postgres
TEST_IMAGE_POSTGRES: 'postgres:16'
collectCoverage: 'true'
include: ${{ fromJSON(inputs.matrix) }}
env:
TEST_IMAGE_POSTGRES: ${{ matrix.TEST_IMAGE_POSTGRES }}
COVERAGE_ENABLED: ${{ matrix.collectCoverage }}
@ -64,6 +51,7 @@ jobs:
# postgres testcontainer) and fails if they differ from what's committed.
# tbls runs as a Docker image in CI.
- name: Verify schema docs are up to date
if: matrix.schema-check-cmd != ''
run: ${{ matrix.schema-check-cmd }}
- name: Upload coverage to Codecov

View File

@ -68,6 +68,12 @@ jobs:
if: fromJSON(steps.paths-filter.outputs.results).db
uses: ./.github/actions/setup-nodejs
- name: Resolve Postgres image
if: fromJSON(steps.paths-filter.outputs.results).db
run: |
IMAGE=$(node -p "require('./packages/testing/containers/postgres-versions.json').primary")
echo "TEST_IMAGE_POSTGRES=$IMAGE" >> "$GITHUB_ENV"
- name: Start Postgres
if: fromJSON(steps.paths-filter.outputs.results).db
uses: isbang/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0

View File

@ -60,6 +60,7 @@
"@testcontainers/postgresql": "catalog:",
"@vitest/coverage-v8": "catalog:",
"express": "catalog:",
"n8n-containers": "workspace:*",
"testcontainers": "catalog:",
"@typescript/native": "catalog:typescript-tooling",
"typescript": "catalog:typescript-tooling",

View File

@ -18,7 +18,7 @@
* a local binary.
*/
import { spawn, spawnSync } from 'node:child_process';
import { mkdirSync, rmSync } from 'node:fs';
import { mkdirSync, readFileSync, rmSync } from 'node:fs';
import { dirname, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
@ -31,6 +31,15 @@ const TMP_DIR = resolve(PKG_ROOT, '.tmp-schema-docs');
const TBLS_IMAGE =
'ghcr.io/k1low/tbls:v1.94.5@sha256:ae8a3bff6d4f8495d13a7982cd71fac3e8a3d1dd394888f2c44ef82216aa14e4';
const POSTGRES_VERSIONS_PATH = resolve(
REPO_ROOT,
'packages/testing/containers/postgres-versions.json',
);
function primaryPostgresImage() {
return JSON.parse(readFileSync(POSTGRES_VERSIONS_PATH, 'utf8')).primary;
}
/** Thrown for expected, user-facing failures so `main`'s cleanup still runs. */
class FailError extends Error {}
@ -122,10 +131,10 @@ async function provision(dbType) {
};
}
// postgres: spin a throwaway container. Image tag matches the testcontainers
// default in `n8n-containers` (packages/testing/containers/test-containers.ts).
// postgres: spin a throwaway container on the version the rest of the test
// tooling defaults to.
const { PostgreSqlContainer } = await import('@testcontainers/postgresql');
const container = await new PostgreSqlContainer('postgres:18-alpine').start();
const container = await new PostgreSqlContainer(primaryPostgresImage()).start();
const conn = {
host: container.getHost(),
port: container.getMappedPort(5432),

View File

@ -2,6 +2,7 @@ import type { Logger } from '@n8n/backend-common';
import type { DatabaseConfig } from '@n8n/config';
import { DataSource } from '@n8n/typeorm';
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import postgresVersions from 'n8n-containers/postgres-versions.json';
import type { ErrorReporter } from 'n8n-core';
import net from 'node:net';
import { getContainerRuntimeClient } from 'testcontainers';
@ -94,11 +95,11 @@ class FreezableProxy {
* a machine with no Postgres and no usable container runtime.
*
* To run it locally, boot a Postgres and export the host, e.g.:
* docker run --rm -e POSTGRES_PASSWORD=password -p 5432:5432 postgres:18-alpine
* docker run --rm -e POSTGRES_PASSWORD=password -p 5432:5432 <POSTGRES_IMAGE>
* DB_POSTGRESDB_HOST=localhost DB_POSTGRESDB_PASSWORD=password \
* pnpm test db-connection-monitor.recovery.postgres
*/
const POSTGRES_IMAGE = 'postgres:18-alpine';
const POSTGRES_IMAGE = postgresVersions.primary;
const CONTAINER_TIMEOUT_MS = 180_000;
const TEST_TIMEOUT_MS = 60_000;

View File

@ -1,3 +1,5 @@
import postgresVersions from 'n8n-containers/postgres-versions.json';
import {
getPostgresVersionWarning,
OLDEST_COMPATIBILITY_POSTGRES_MAJOR,
@ -36,3 +38,40 @@ describe('getPostgresVersionWarning', () => {
expect(getPostgresVersionWarning('')).toBeNull();
});
});
// These thresholds and the DB test matrix move together every November.
describe('policy thresholds vs the versions CI tests', () => {
const tested = postgresVersions.matrix;
it('should stay silent for every fully supported major CI tests', () => {
const supported = tested.filter(({ support }) => support === 'supported');
for (const { major } of supported) {
expect(getPostgresVersionWarning(`${major}.0`), `Postgres ${major}`).toBeNull();
}
});
it('should warn about compatibility support only for the compatibility major CI tests', () => {
const compatibility = tested.filter(({ support }) => support === 'compatibility');
for (const { major } of compatibility) {
expect(getPostgresVersionWarning(`${major}.0`), `Postgres ${major}`).toContain(
'compatibility support only',
);
}
});
it('should treat the oldest tested major as the compatibility major', () => {
const oldest = Math.min(...tested.map(({ major }) => major));
expect(oldest).toBe(OLDEST_COMPATIBILITY_POSTGRES_MAJOR);
});
it('should treat the oldest fully supported tested major as the supported floor', () => {
const supported = tested
.filter(({ support }) => support === 'supported')
.map(({ major }) => major);
expect(Math.min(...supported)).toBe(OLDEST_SUPPORTED_POSTGRES_MAJOR);
});
});

View File

@ -40,6 +40,7 @@
"@types/express": "catalog:",
"@types/pg": "^8.15.6",
"@types/supertest": "^6.0.3",
"n8n-containers": "workspace:*",
"supertest": "^7.1.1",
"testcontainers": "catalog:",
"typescript": "catalog:typescript",

View File

@ -1,5 +1,6 @@
import type { DataSource } from '@n8n/typeorm';
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import postgresVersions from 'n8n-containers/postgres-versions.json';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { createDataSource } from '../data-source';
@ -10,7 +11,7 @@ describe('workflow_execution table (integration)', () => {
let dataSource: DataSource;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:18-alpine').start();
container = await new PostgreSqlContainer(postgresVersions.primary).start();
dataSource = createDataSource(container.getConnectionUri());
await dataSource.initialize();
await dataSource.runMigrations();

View File

@ -1,5 +1,6 @@
import type { DataSource } from '@n8n/typeorm';
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import postgresVersions from 'n8n-containers/postgres-versions.json';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import type { StepStatus } from '../../execution/execution.types';
@ -14,7 +15,7 @@ describe('workflow_step_execution table (integration)', () => {
let dataSource: DataSource;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:18-alpine').start();
container = await new PostgreSqlContainer(postgresVersions.primary).start();
dataSource = createDataSource(container.getConnectionUri());
await dataSource.initialize();
await dataSource.runMigrations();

View File

@ -1,5 +1,6 @@
import type { DataSource } from '@n8n/typeorm';
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import postgresVersions from 'n8n-containers/postgres-versions.json';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { AllowAllAdmittance } from '../../admittance';
@ -35,7 +36,7 @@ describe('execution start (integration)', () => {
let dataSource: DataSource;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:18-alpine').start();
container = await new PostgreSqlContainer(postgresVersions.primary).start();
dataSource = createDataSource(container.getConnectionUri());
await dataSource.initialize();
await dataSource.runMigrations();

View File

@ -1,5 +1,6 @@
import type { DataSource } from '@n8n/typeorm';
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import postgresVersions from 'n8n-containers/postgres-versions.json';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { AllowAllAdmittance } from '../../admittance';
@ -34,7 +35,7 @@ describe('step execution (integration)', () => {
let dataSource: DataSource;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:18-alpine').start();
container = await new PostgreSqlContainer(postgresVersions.primary).start();
dataSource = createDataSource(container.getConnectionUri());
await dataSource.initialize();
await dataSource.runMigrations();

View File

@ -1,5 +1,6 @@
import type { DataSource } from '@n8n/typeorm';
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import postgresVersions from 'n8n-containers/postgres-versions.json';
import request from 'supertest';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
@ -22,7 +23,7 @@ describe('POST /api/workflow-executions (integration)', () => {
let stop: () => Promise<void>;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:18-alpine').start();
container = await new PostgreSqlContainer(postgresVersions.primary).start();
dataSource = createDataSource(container.getConnectionUri());
await dataSource.initialize();
await dataSource.runMigrations();

View File

@ -544,16 +544,20 @@ describe('CredentialsRepository', () => {
const projectRoles = await roleService.rolesWithScope('project', scopes);
const credentialRoles = await roleService.rolesWithScope('credential', scopes);
// Both approaches need an explicit order: without one, Postgres is free to
// return the rows in any order, and the two queries use different plans.
const oldOptions = {
filter: { projectId: teamProject.id, name: 'Test' },
take: 2,
skip: 0,
order: { id: 'ASC' as const },
};
const newOptions = {
filter: { name: 'Test' },
take: 2,
skip: 0,
order: { id: 'ASC' as const },
};
// ACT - Old Approach
@ -574,7 +578,7 @@ describe('CredentialsRepository', () => {
expect(newResult.count).toBe(oldResult[1]);
expect(newResult.credentials).toHaveLength(oldResult[0].length);
// Check same credentials in same order (sorting should be consistent)
// Check same credentials in same order
const oldIds = oldResult[0].map((c) => c.id);
const newIds = newResult.credentials.map((c) => c.id);
expect(newIds).toEqual(oldIds);

View File

@ -91,7 +91,9 @@ pnpm stack:helm --image ghcr.io/n8n-io/n8n:ci-12345
### CLI Options
```
--mode <mode> standalone (SQLite, default) or queue (PostgreSQL + Redis + workers)
--mode <mode> standalone (SQLite, default) or queue (PostgreSQL + Redis + workers).
Queue mode's Postgres chart and image are pinned in
postgres-versions.json.
--image <image> n8n Docker image (default: n8nio/n8n:local)
--chart-ref <ref> Git branch/tag for n8n-hosting (default: main)
--chart-repo <url> Git repo URL (default: https://github.com/n8n-io/n8n-hosting.git)

View File

@ -349,7 +349,7 @@ Now usable as `test.use({ capability: 'my-capability' })`.
| Service | Helper | Description |
|---------|--------|-------------|
| `postgres` | - | PostgreSQL database |
| `postgres` | - | PostgreSQL database (version from `postgres-versions.json`) |
| `redis` | - | Redis for queue mode |
| `mailpit` | ✓ | Email testing (SMTP + UI) |
| `gitea` | ✓ | Git server for source control |

View File

@ -12,6 +12,7 @@ import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import { setTimeout as wait } from 'node:timers/promises';
import postgresVersions from './postgres-versions.json';
import { TEST_CONTAINER_IMAGES } from './test-containers';
const DEFAULT_K3S_IMAGE = 'rancher/k3s:v1.32.2-k3s1';
@ -236,9 +237,13 @@ function deployQueueInfrastructure(env: NodeJS.ProcessEnv): void {
'Add Bitnami repo',
);
log('Deploying PostgreSQL...');
// By digest because the Bitnami catalog only publishes `postgresql:latest`,
// which the chart defaults to.
const { chartVersion, imageDigest } = postgresVersions.helm;
log(`Deploying PostgreSQL (chart ${chartVersion})...`);
execOnHost(
'helm install postgresql bitnami/postgresql --set auth.username=n8n --set auth.password=n8n-test-password --set auth.database=n8n --set primary.resources.requests.cpu=100m --set primary.resources.requests.memory=256Mi --set primary.resources.limits.cpu=500m --set primary.resources.limits.memory=512Mi --wait --timeout 3m',
`helm install postgresql bitnami/postgresql --version ${chartVersion} --set image.digest=${imageDigest} --set auth.username=n8n --set auth.password=n8n-test-password --set auth.database=n8n --set primary.resources.requests.cpu=100m --set primary.resources.requests.memory=256Mi --set primary.resources.limits.cpu=500m --set primary.resources.limits.memory=512Mi --wait --timeout 3m`,
env,
'Deploy PostgreSQL',
);

View File

@ -0,0 +1,12 @@
{
"primary": "postgres:18.4-alpine",
"matrix": [
{ "major": 16, "support": "compatibility", "image": "postgres:16.14-alpine" },
{ "major": 17, "support": "supported", "image": "postgres:17.10-alpine" },
{ "major": 18, "support": "supported", "image": "postgres:18.4-alpine" }
],
"helm": {
"chartVersion": "18.8.7",
"imageDigest": "sha256:09a0a8dc1c52c5a0c4ccc7febe3d4d4df1e5d50816e2312ef0e21d3c09084ec0"
}
}

View File

@ -20,9 +20,11 @@
* N8N_DOCKER_IMAGE is also supported for backwards compatibility.
*/
import postgresVersions from './postgres-versions.json';
/** Default images - override via TEST_IMAGE_<KEY> env vars */
const DEFAULT_IMAGES = {
postgres: 'postgres:18-alpine',
postgres: postgresVersions.primary,
redis: 'redis:alpine',
caddy: 'caddy:alpine',
n8n: 'n8nio/n8n:local',

View File

@ -2161,6 +2161,9 @@ importers:
express:
specifier: 'catalog:'
version: 5.1.0
n8n-containers:
specifier: workspace:*
version: link:../../testing/containers
testcontainers:
specifier: 'catalog:'
version: 11.13.0
@ -2290,6 +2293,9 @@ importers:
'@types/supertest':
specifier: ^6.0.3
version: 6.0.3
n8n-containers:
specifier: workspace:*
version: link:../../testing/containers
supertest:
specifier: ^7.1.1
version: 7.1.1