ci: Drop support for Node.js 18 (#15146)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2025-06-04 15:54:57 +02:00
committed by GitHub
parent 40de4ed91c
commit 3bdbdfe6ce
39 changed files with 432 additions and 500 deletions

View File

@@ -53,7 +53,7 @@ body:
id: nodejs-version id: nodejs-version
attributes: attributes:
label: Node.js Version label: Node.js Version
placeholder: ex. 18.16.0 placeholder: ex. 20.19.0
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@@ -46,7 +46,7 @@ jobs:
needs: install-and-build needs: install-and-build
strategy: strategy:
matrix: matrix:
node-version: [18.x, 20.x, 22.4] node-version: [20.x, 22.x]
with: with:
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
nodeVersion: ${{ matrix.node-version }} nodeVersion: ${{ matrix.node-version }}

View File

@@ -9,7 +9,6 @@ on:
required: true required: true
default: '20' default: '20'
options: options:
- '18'
- '20' - '20'
- '22' - '22'

View File

@@ -34,7 +34,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version: '18' node-version: 20.x
cache: 'pnpm' cache: 'pnpm'
- name: Cache build artifacts - name: Cache build artifacts

View File

@@ -76,7 +76,13 @@ jobs:
with: with:
ref: ${{ steps.calculate_ref.outputs.value }} ref: ${{ steps.calculate_ref.outputs.value }}
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.0.0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20.x
cache: 'pnpm'
- name: Cache build artifacts - name: Cache build artifacts
id: cache-build-artifacts id: cache-build-artifacts
@@ -94,7 +100,7 @@ jobs:
- name: Cypress build - name: Cypress build
if: steps.cache-build-artifacts.outputs.cache-hit != 'true' if: steps.cache-build-artifacts.outputs.cache-hit != 'true'
uses: cypress-io/github-action@0ee1130f05f69098ab5c560bd198fecf5a14d75b # v6.9.0 uses: cypress-io/github-action@be1bab96b388bbd9ce3887e397d373c8557e15af # v6.9.2
with: with:
# Disable running of tests within install job # Disable running of tests within install job
runTests: false runTests: false
@@ -120,7 +126,13 @@ jobs:
with: with:
ref: ${{ steps.calculate_ref.outputs.value }} ref: ${{ steps.calculate_ref.outputs.value }}
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.0.0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20.x
cache: 'pnpm'
- name: Restore cached pnpm modules - name: Restore cached pnpm modules
id: cache-build-artifacts id: cache-build-artifacts
@@ -136,7 +148,7 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Cypress run - name: Cypress run
uses: cypress-io/github-action@0ee1130f05f69098ab5c560bd198fecf5a14d75b # v6.9.0 uses: cypress-io/github-action@be1bab96b388bbd9ce3887e397d373c8557e15af # v6.9.2
with: with:
working-directory: cypress working-directory: cypress
install: false install: false

View File

@@ -17,7 +17,7 @@ jobs:
is_pr_approved_by_maintainer: true is_pr_approved_by_maintainer: true
run-e2e-tests: run-e2e-tests:
name: E2E [Electron/Node 18] name: E2E
uses: ./.github/workflows/e2e-reusable.yml uses: ./.github/workflows/e2e-reusable.yml
needs: [eligibility_check] needs: [eligibility_check]
if: needs.eligibility_check.outputs.should_run == 'true' if: needs.eligibility_check.outputs.should_run == 'true'
@@ -28,7 +28,7 @@ jobs:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
post-e2e-tests: post-e2e-tests:
name: E2E [Electron/Node 18] - Checks name: E2E - Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [eligibility_check, run-e2e-tests] needs: [eligibility_check, run-e2e-tests]
if: always() && needs.eligibility_check.result != 'skipped' if: always() && needs.eligibility_check.result != 'skipped'

View File

@@ -40,7 +40,7 @@ jobs:
shell: bash shell: bash
run-e2e-tests: run-e2e-tests:
name: E2E [Electron/Node 18] name: E2E
uses: ./.github/workflows/e2e-reusable.yml uses: ./.github/workflows/e2e-reusable.yml
with: with:
branch: ${{ github.event.inputs.branch || 'master' }} branch: ${{ github.event.inputs.branch || 'master' }}

View File

@@ -23,11 +23,11 @@
"n8n-workflow": "workspace:*" "n8n-workflow": "workspace:*"
}, },
"dependencies": { "dependencies": {
"@ngneat/falso": "^7.2.0", "@ngneat/falso": "^7.3.0",
"@sinonjs/fake-timers": "^13.0.2", "@sinonjs/fake-timers": "^13.0.2",
"cypress": "^13.14.2", "cypress": "^14.4.0",
"cypress-otp": "^1.0.3", "cypress-otp": "^1.0.3",
"cypress-real-events": "^1.13.0", "cypress-real-events": "^1.14.0",
"flatted": "catalog:", "flatted": "catalog:",
"lodash": "catalog:", "lodash": "catalog:",
"nanoid": "catalog:", "nanoid": "catalog:",

View File

@@ -12,6 +12,18 @@ before(() => {
Cypress.on('uncaught:exception', (error) => { Cypress.on('uncaught:exception', (error) => {
return !error.message.includes('ResizeObserver'); return !error.message.includes('ResizeObserver');
}); });
// Mock the clipboard API because in newer versions of cypress the clipboard API is flaky when the window is not focussed.
Cypress.on('window:before:load', (win) => {
let currentContent: string = '';
Object.assign(win.navigator.clipboard, {
writeText: async (text: string) => {
currentContent = text;
return await Promise.resolve();
},
readText: async () => await Promise.resolve(currentContent),
});
});
}); });
beforeEach(() => { beforeEach(() => {

View File

@@ -3,7 +3,7 @@
"version": "1.97.0", "version": "1.97.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=20.15", "node": ">=22.16",
"pnpm": ">=10.2.1" "pnpm": ">=10.2.1"
}, },
"packageManager": "pnpm@10.2.1", "packageManager": "pnpm@10.2.1",
@@ -79,7 +79,7 @@
], ],
"overrides": { "overrides": {
"@azure/identity": "^4.3.0", "@azure/identity": "^4.3.0",
"@types/node": "^18.16.16", "@types/node": "^20.17.50",
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"pug": "^3.0.3", "pug": "^3.0.3",

View File

@@ -1,6 +1,5 @@
/** @type {import('jest').Config} */ /** @type {import('jest').Config} */
module.exports = { module.exports = {
...require('../../../jest.config'), ...require('../../../jest.config'),
setupFilesAfterEnv: ['n8n-workflow/test/setup.ts'],
testTimeout: 10_000, testTimeout: 10_000,
}; };

View File

@@ -20,7 +20,7 @@
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\"" "watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\""
}, },
"engines": { "engines": {
"node": ">=20.10" "node": ">=20.19"
}, },
"keywords": [ "keywords": [
"automate", "automate",

View File

@@ -2,5 +2,5 @@
module.exports = { module.exports = {
...require('../../../jest.config'), ...require('../../../jest.config'),
collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'], collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'],
setupFilesAfterEnv: ['jest-expect-message', 'n8n-workflow/test/setup.ts'], setupFilesAfterEnv: ['jest-expect-message'],
}; };

View File

@@ -1,6 +1,5 @@
/** @type {import('jest').Config} */ /** @type {import('jest').Config} */
module.exports = { module.exports = {
...require('../../../jest.config'), ...require('../../../jest.config'),
setupFilesAfterEnv: ['n8n-workflow/test/setup.ts'],
testTimeout: 10_000, testTimeout: 10_000,
}; };

View File

@@ -1,5 +0,0 @@
// WebCrypto Polyfill for older versions of Node.js 18
if (!globalThis.crypto?.getRandomValues) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
globalThis.crypto = require('node:crypto').webcrypto;
}

View File

@@ -1,4 +1,3 @@
import './polyfills';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { ensureError, setGlobalState } from 'n8n-workflow'; import { ensureError, setGlobalState } from 'n8n-workflow';

View File

@@ -16,6 +16,18 @@ If you've been ingesting route metrics from your n8n instance (version 1.81.0 an
how the `last_activity` metric has affected your Prometheus instance and potentially clean up the old data. Future how the `last_activity` metric has affected your Prometheus instance and potentially clean up the old data. Future
metrics will also be served in a different format, which needs to be taken into account. metrics will also be served in a different format, which needs to be taken into account.
### What changed?
The minimum Node.js version required for n8n is now v20.
### When is action necessary?
If you're using n8n via npm or PM2 or if you're contributing to n8n.
### How to upgrade:
Update the Node.js version to v20 or above.
## 1.83.0 ## 1.83.0
### What changed? ### What changed?

View File

@@ -65,11 +65,6 @@ if (process.env.NODEJS_PREFER_IPV4 === 'true') {
// More details: https://github.com/nodejs/node/issues/48145 // More details: https://github.com/nodejs/node/issues/48145
require('net').setDefaultAutoSelectFamily?.(false); require('net').setDefaultAutoSelectFamily?.(false);
// WebCrypto Polyfill for older versions of Node.js 18
if (!globalThis.crypto?.getRandomValues) {
globalThis.crypto = require('node:crypto').webcrypto;
}
(async () => { (async () => {
const oclif = await import('@oclif/core'); const oclif = await import('@oclif/core');
await oclif.execute({ dir: __dirname }); await oclif.execute({ dir: __dirname });

View File

@@ -7,7 +7,6 @@ module.exports = {
globalSetup: '<rootDir>/test/setup.ts', globalSetup: '<rootDir>/test/setup.ts',
globalTeardown: '<rootDir>/test/teardown.ts', globalTeardown: '<rootDir>/test/teardown.ts',
setupFilesAfterEnv: [ setupFilesAfterEnv: [
'n8n-workflow/test/setup.ts',
'<rootDir>/test/setup-test-folder.ts', '<rootDir>/test/setup-test-folder.ts',
'<rootDir>/test/setup-mocks.ts', '<rootDir>/test/setup-mocks.ts',
'<rootDir>/test/extend-expect.ts', '<rootDir>/test/extend-expect.ts',

View File

@@ -44,7 +44,7 @@
"workflow" "workflow"
], ],
"engines": { "engines": {
"node": ">=18.17 <= 22" "node": ">=20.19 <= 22.x"
}, },
"files": [ "files": [
"bin", "bin",
@@ -69,7 +69,7 @@
"@types/psl": "^1.1.0", "@types/psl": "^1.1.0",
"@types/replacestream": "^4.0.1", "@types/replacestream": "^4.0.1",
"@types/shelljs": "^0.8.11", "@types/shelljs": "^0.8.11",
"@types/sshpk": "^1.17.1", "@types/sshpk": "^1.17.4",
"@types/superagent": "^8.1.9", "@types/superagent": "^8.1.9",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"@types/syslog-client": "^1.1.2", "@types/syslog-client": "^1.1.2",
@@ -170,7 +170,7 @@
"simple-git": "3.17.0", "simple-git": "3.17.0",
"source-map-support": "0.5.21", "source-map-support": "0.5.21",
"sqlite3": "5.1.7", "sqlite3": "5.1.7",
"sshpk": "1.17.0", "sshpk": "1.18.0",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"syslog-client": "1.1.1", "syslog-client": "1.1.1",
"uuid": "catalog:", "uuid": "catalog:",

View File

@@ -21,7 +21,7 @@ type ConnectionState = {
export class DbConnection { export class DbConnection {
private dataSource: DataSource; private dataSource: DataSource;
private pingTimer: NodeJS.Timer | undefined; private pingTimer: NodeJS.Timeout | undefined;
readonly connectionState: ConnectionState = { readonly connectionState: ConnectionState = {
connected: false, connected: false,

View File

@@ -61,7 +61,7 @@ export class MessageEventBus extends EventEmitter {
[key: string]: MessageEventBusDestination; [key: string]: MessageEventBusDestination;
} = {}; } = {};
private pushIntervalTimer: NodeJS.Timer; private pushIntervalTimer: NodeJS.Timeout;
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,

View File

@@ -28,9 +28,9 @@ export class ExternalSecretsManager {
initialized = false; initialized = false;
updateInterval: NodeJS.Timer; updateInterval: NodeJS.Timeout;
initRetryTimeouts: Record<string, NodeJS.Timer> = {}; initRetryTimeouts: Record<string, NodeJS.Timeout> = {};
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,

View File

@@ -230,7 +230,7 @@ export class VaultProvider extends SecretsProvider {
#http: AxiosInstance; #http: AxiosInstance;
private refreshTimeout: NodeJS.Timer | null; private refreshTimeout: NodeJS.Timeout | null;
private refreshAbort = new AbortController(); private refreshAbort = new AbortController();

View File

@@ -11,7 +11,7 @@ import { InsightsConfig } from './insights.config';
*/ */
@Service() @Service()
export class InsightsCompactionService { export class InsightsCompactionService {
private compactInsightsTimer: NodeJS.Timer | undefined; private compactInsightsTimer: NodeJS.Timeout | undefined;
constructor( constructor(
private readonly insightsByPeriodRepository: InsightsByPeriodRepository, private readonly insightsByPeriodRepository: InsightsByPeriodRepository,

View File

@@ -45,7 +45,7 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
private readonly leaderKeyTtl = this.globalConfig.multiMainSetup.ttl; private readonly leaderKeyTtl = this.globalConfig.multiMainSetup.ttl;
private leaderCheckInterval: NodeJS.Timer | undefined; private leaderCheckInterval: NodeJS.Timeout | undefined;
async init() { async init() {
const prefix = config.getEnv('redis.prefix'); const prefix = config.getEnv('redis.prefix');

View File

@@ -39,7 +39,7 @@ export class Subscriber {
const debouncedHandlerFn = debounce(handlerFn, 300); const debouncedHandlerFn = debounce(handlerFn, 300);
this.client.on('message', (channel: PubSub.Channel, str) => { this.client.on('message', (channel: PubSub.Channel, str: string) => {
const msg = this.parseMessage(str, channel); const msg = this.parseMessage(str, channel);
if (!msg) return; if (!msg) return;
if (msg.debounce) debouncedHandlerFn(msg); if (msg.debounce) debouncedHandlerFn(msg);

View File

@@ -380,7 +380,7 @@ export class ScalingService {
private readonly jobCounters = { completed: 0, failed: 0 }; private readonly jobCounters = { completed: 0, failed: 0 };
/** Interval for collecting queue metrics to expose via Prometheus. */ /** Interval for collecting queue metrics to expose via Prometheus. */
private queueMetricsInterval: NodeJS.Timer | undefined; private queueMetricsInterval: NodeJS.Timeout | undefined;
get isQueueMetricsEnabled() { get isQueueMetricsEnabled() {
return ( return (

View File

@@ -26,7 +26,7 @@ import { DbConnection } from '@/databases/db-connection';
@Service() @Service()
export class ExecutionsPruningService { export class ExecutionsPruningService {
/** Timer for soft-deleting executions on a rolling basis. */ /** Timer for soft-deleting executions on a rolling basis. */
private softDeletionInterval: NodeJS.Timer | undefined; private softDeletionInterval: NodeJS.Timeout | undefined;
/** Timeout for next hard-deletion of soft-deleted executions. */ /** Timeout for next hard-deletion of soft-deleted executions. */
private hardDeletionTimeout: NodeJS.Timeout | undefined; private hardDeletionTimeout: NodeJS.Timeout | undefined;

View File

@@ -53,7 +53,7 @@ export class RedisClientService extends TypedEmitter<RedisEventMap> {
? this.createClusterClient(arg) ? this.createClusterClient(arg)
: this.createRegularClient(arg); : this.createRegularClient(arg);
client.on('error', (error) => { client.on('error', (error: Error) => {
if ('code' in error && error.code === 'ECONNREFUSED') return; // handled by retryStrategy if ('code' in error && error.code === 'ECONNREFUSED') return; // handled by retryStrategy
this.logger.error(`[Redis client] ${error.message}`, { error }); this.logger.error(`[Redis client] ${error.message}`, { error });

View File

@@ -31,7 +31,7 @@ type WsStatusCode = (typeof WsStatusCodes)[keyof typeof WsStatusCodes];
export class TaskBrokerWsServer { export class TaskBrokerWsServer {
runnerConnections: Map<TaskRunner['id'], WebSocket> = new Map(); runnerConnections: Map<TaskRunner['id'], WebSocket> = new Map();
private heartbeatTimer: NodeJS.Timer | undefined; private heartbeatTimer: NodeJS.Timeout | undefined;
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,

View File

@@ -1,6 +1 @@
import 'reflect-metadata'; import 'reflect-metadata';
// WebCrypto Polyfill for older versions of Node.js 18
if (!globalThis.crypto?.getRandomValues) {
globalThis.crypto = require('node:crypto').webcrypto;
}

View File

@@ -10,7 +10,7 @@ import WorkerChartsAccordion from './WorkerChartsAccordion.ee.vue';
import { sortByProperty } from '@n8n/utils/sort/sortByProperty'; import { sortByProperty } from '@n8n/utils/sort/sortByProperty';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
let interval: NodeJS.Timer; let interval: NodeJS.Timeout;
const orchestrationStore = useOrchestrationStore(); const orchestrationStore = useOrchestrationStore();

View File

@@ -14,7 +14,7 @@ export interface IOrchestrationStoreState {
[id: string]: IWorkerHistoryItem[]; [id: string]: IWorkerHistoryItem[];
}; };
workersLastUpdated: { [id: string]: number }; workersLastUpdated: { [id: string]: number };
statusInterval: NodeJS.Timer | null; statusInterval: NodeJS.Timeout | null;
} }
export interface IWorkerHistoryItem { export interface IWorkerHistoryItem {

View File

@@ -6,9 +6,5 @@ module.exports = {
...require('../../jest.config'), ...require('../../jest.config'),
collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'], collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'],
globalSetup: '<rootDir>/test/globalSetup.ts', globalSetup: '<rootDir>/test/globalSetup.ts',
setupFilesAfterEnv: [ setupFilesAfterEnv: ['jest-expect-message', '<rootDir>/test/setup.ts'],
'jest-expect-message',
'n8n-workflow/test/setup.ts',
'<rootDir>/test/setup.ts',
],
}; };

View File

@@ -6,7 +6,7 @@ import { NodeOperationError, type IExecuteFunctions } from 'n8n-workflow';
import { Wait } from '../Wait.node'; import { Wait } from '../Wait.node';
describe('Execute Wait Node', () => { describe('Execute Wait Node', () => {
let timer: NodeJS.Timer; let timer: NodeJS.Timeout;
const { clearInterval, setInterval } = global; const { clearInterval, setInterval } = global;
const nextDay = DateTime.now().startOf('day').plus({ days: 1 }); const nextDay = DateTime.now().startOf('day').plus({ days: 1 });

View File

@@ -1,5 +1,4 @@
/** @type {import('jest').Config} */ /** @type {import('jest').Config} */
module.exports = { module.exports = {
...require('../../jest.config'), ...require('../../jest.config'),
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
}; };

View File

@@ -1,7 +0,0 @@
import { randomFillSync } from 'crypto';
Object.defineProperty(globalThis, 'crypto', {
value: {
getRandomValues: (buffer: NodeJS.ArrayBufferView) => randomFillSync(buffer),
},
});

792
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff