mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
ci: Run DB tests against the full supported Postgres range (#35960)
This commit is contained in:
parent
acd4fdacc9
commit
4afbd16dc7
1
.github/WORKFLOWS.md
vendored
1
.github/WORKFLOWS.md
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
6
.github/docker-compose.yml
vendored
6
.github/docker-compose.yml
vendored
|
|
@ -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
95
.github/scripts/db-test-matrix.mjs
vendored
Normal 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
128
.github/scripts/db-test-matrix.test.mjs
vendored
Normal 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/);
|
||||
});
|
||||
});
|
||||
11
.github/workflows/ci-pull-requests.yml
vendored
11
.github/workflows/ci-pull-requests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
24
.github/workflows/test-db-reusable.yml
vendored
24
.github/workflows/test-db-reusable.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/util-data-tooling.yml
vendored
6
.github/workflows/util-data-tooling.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
12
packages/testing/containers/postgres-versions.json
Normal file
12
packages/testing/containers/postgres-versions.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user