mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-28 07:17:04 +02:00
feat(core): Add migration-timestamp code-health rule (no-changelog) (#30388)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5d872d1375
commit
2e8f08df32
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": 1,
|
||||
"generated": "2026-05-12T09:37:31.489Z",
|
||||
"totalViolations": 82,
|
||||
"generated": "2026-05-13T10:23:52.870Z",
|
||||
"totalViolations": 87,
|
||||
"violations": {
|
||||
"packages/@n8n/ai-workflow-builder.ee/package.json": [
|
||||
{
|
||||
|
|
@ -52,13 +52,25 @@
|
|||
"packages/@n8n/nodes-langchain/package.json": [
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 292,
|
||||
"line": 266,
|
||||
"message": "@n8n/typeorm@0.3.20-16 should use \"catalog:\" (exists in pnpm-workspace.yaml)",
|
||||
"hash": "a7c1bca1c439"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 267,
|
||||
"message": "mysql2@3.17.0 should use \"catalog:\" (exists in pnpm-workspace.yaml)",
|
||||
"hash": "2d7d1b715d0c"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 293,
|
||||
"message": "openai@^6.34.0 should use \"catalog:\" (exists in pnpm-workspace.yaml)",
|
||||
"hash": "3c1f53f0afe3"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 299,
|
||||
"line": 300,
|
||||
"message": "tmp-promise appears in 4 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "88d67e2ef747"
|
||||
},
|
||||
|
|
@ -70,25 +82,25 @@
|
|||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 274,
|
||||
"line": 275,
|
||||
"message": "cheerio appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "8cd029bb871e"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 284,
|
||||
"line": 285,
|
||||
"message": "jsdom appears in 4 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "26f20ebea4b1"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 289,
|
||||
"line": 290,
|
||||
"message": "mongodb appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "46cb48884e22"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 293,
|
||||
"line": 294,
|
||||
"message": "pdf-parse appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "0c7d44a9c2e4"
|
||||
}
|
||||
|
|
@ -220,49 +232,49 @@
|
|||
"packages/@n8n/instance-ai/package.json": [
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 80,
|
||||
"line": 81,
|
||||
"message": "@ai-sdk/anthropic appears in 3 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "5b2153508e47"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 86,
|
||||
"line": 87,
|
||||
"message": "@types/psl appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "56dabb51b433"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 56,
|
||||
"line": 57,
|
||||
"message": "@mozilla/readability appears in 5 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "8fa6b9a8fc91"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 64,
|
||||
"line": 65,
|
||||
"message": "csv-parse appears in 3 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "8f082fc2e8b6"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 71,
|
||||
"line": 72,
|
||||
"message": "turndown appears in 3 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "9a9d97065952"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 87,
|
||||
"line": 88,
|
||||
"message": "@types/turndown appears in 3 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "12e346c47b39"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 50,
|
||||
"line": 51,
|
||||
"message": "@joplin/turndown-plugin-gfm appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "a3cf1504b5c2"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 68,
|
||||
"line": 69,
|
||||
"message": "pdf-parse appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "283fa9114c03"
|
||||
}
|
||||
|
|
@ -290,61 +302,61 @@
|
|||
"packages/nodes-base/package.json": [
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 911,
|
||||
"line": 913,
|
||||
"message": "change-case appears in 5 packages with 3 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "2d1fab7a5b05"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 961,
|
||||
"line": 963,
|
||||
"message": "semver appears in 4 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "2daf37aa14e4"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 966,
|
||||
"line": 968,
|
||||
"message": "tmp-promise appears in 4 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "3f93c404ae9c"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 900,
|
||||
"line": 902,
|
||||
"message": "@mozilla/readability appears in 5 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "ca4ac788adc6"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 912,
|
||||
"line": 914,
|
||||
"message": "cheerio appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "1a1b5bbc50c9"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 915,
|
||||
"line": 917,
|
||||
"message": "csv-parse appears in 3 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "781db4a1e068"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 917,
|
||||
"line": 919,
|
||||
"message": "eventsource appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "9795e6c6d9e9"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 930,
|
||||
"line": 932,
|
||||
"message": "jsdom appears in 4 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "02341f2b5e3e"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 941,
|
||||
"line": 943,
|
||||
"message": "mongodb appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "f688907d087a"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 892,
|
||||
"line": 894,
|
||||
"message": "eslint-plugin-n8n-nodes-base appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "ac254baa61f9"
|
||||
}
|
||||
|
|
@ -384,19 +396,19 @@
|
|||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 90,
|
||||
"line": 91,
|
||||
"message": "prettier appears in 3 packages with 3 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "9e9c7ec09a0b"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 92,
|
||||
"line": 93,
|
||||
"message": "semver appears in 4 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "d8c606e42c92"
|
||||
},
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 77,
|
||||
"line": 78,
|
||||
"message": "esprima-next appears in 3 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "62156c2613b2"
|
||||
}
|
||||
|
|
@ -491,6 +503,14 @@
|
|||
"hash": "733c3960022e"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/computer-use/package.json": [
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 47,
|
||||
"message": "eventsource appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "f50c1eee2ed6"
|
||||
}
|
||||
],
|
||||
"packages/workflow/package.json": [
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
|
|
@ -511,14 +531,6 @@
|
|||
"hash": "b660317b5f6f"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/computer-use/package.json": [
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
"line": 47,
|
||||
"message": "eventsource appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "f50c1eee2ed6"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/eslint-plugin-community-nodes/package.json": [
|
||||
{
|
||||
"rule": "catalog-violations",
|
||||
|
|
@ -540,6 +552,30 @@
|
|||
"message": "stylelint appears in 2 packages with 2 different versions — add to pnpm-workspace.yaml catalog",
|
||||
"hash": "955f3fe044c7"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/db/src/migrations/common/1783000000000-CreateAgentTables.ts": [
|
||||
{
|
||||
"rule": "migration-timestamp",
|
||||
"line": 1,
|
||||
"message": "1783000000000-CreateAgentTables.ts is prefixed with a future timestamp (1783000000000, ~50d ahead of now). Migration prefixes must be the exact Date.now() value at the time of creation — AI agents commonly fabricate future timestamps.",
|
||||
"hash": "d81428d567aa"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/db/src/migrations/common/1783000000001-CreateAgentExecutionTables.ts": [
|
||||
{
|
||||
"rule": "migration-timestamp",
|
||||
"line": 1,
|
||||
"message": "1783000000001-CreateAgentExecutionTables.ts is prefixed with a future timestamp (1783000000001, ~50d ahead of now). Migration prefixes must be the exact Date.now() value at the time of creation — AI agents commonly fabricate future timestamps.",
|
||||
"hash": "e33a8a794126"
|
||||
}
|
||||
],
|
||||
"packages/@n8n/db/src/migrations/common/1784000000000-CreateAgentObservationTables.ts": [
|
||||
{
|
||||
"rule": "migration-timestamp",
|
||||
"line": 1,
|
||||
"message": "1784000000000-CreateAgentObservationTables.ts is prefixed with a future timestamp (1784000000000, ~61d ahead of now). Migration prefixes must be the exact Date.now() value at the time of creation — AI agents commonly fabricate future timestamps.",
|
||||
"hash": "4bae5ef065ad"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import type { RuleSettingsMap } from '@n8n/rules-engine';
|
|||
|
||||
import type { CodeHealthContext } from './context.js';
|
||||
import { CatalogViolationsRule } from './rules/catalog-violations.rule.js';
|
||||
import { MigrationTimestampRule } from './rules/migration-timestamp.rule.js';
|
||||
import { WorkflowPrTargetSafetyRule } from './rules/workflow-pr-target-safety.rule.js';
|
||||
|
||||
export type { CodeHealthContext } from './context.js';
|
||||
export { CatalogViolationsRule } from './rules/catalog-violations.rule.js';
|
||||
export { MigrationTimestampRule } from './rules/migration-timestamp.rule.js';
|
||||
export { WorkflowPrTargetSafetyRule } from './rules/workflow-pr-target-safety.rule.js';
|
||||
|
||||
const defaultRuleSettings: RuleSettingsMap = {
|
||||
|
|
@ -20,6 +22,11 @@ const defaultRuleSettings: RuleSettingsMap = {
|
|||
severity: 'error',
|
||||
options: { allowedWorkflows: ['ci-cla-check.yml'] },
|
||||
},
|
||||
'migration-timestamp': {
|
||||
enabled: true,
|
||||
severity: 'error',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
function mergeSettings(defaults: RuleSettingsMap, overrides?: RuleSettingsMap): RuleSettingsMap {
|
||||
|
|
@ -39,6 +46,7 @@ export function createDefaultRunner(settings?: RuleSettingsMap): RuleRunner<Code
|
|||
const runner = new RuleRunner<CodeHealthContext>();
|
||||
runner.registerRule(new CatalogViolationsRule());
|
||||
runner.registerRule(new WorkflowPrTargetSafetyRule());
|
||||
runner.registerRule(new MigrationTimestampRule());
|
||||
runner.applySettings(mergeSettings(defaultRuleSettings, settings));
|
||||
return runner;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import type { CodeHealthContext } from '../context.js';
|
||||
import { MigrationTimestampRule } from './migration-timestamp.rule.js';
|
||||
|
||||
const NOW = 1_777_000_000_000; // fixed reference time used in assertions
|
||||
|
||||
const MIGRATIONS_DIR = path.join('packages', '@n8n', 'db', 'src', 'migrations', 'postgresdb');
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'code-health-migration-test-'));
|
||||
}
|
||||
|
||||
function writeMigration(rootDir: string, fileName: string, content = 'export {};\n'): void {
|
||||
const fullPath = path.join(rootDir, MIGRATIONS_DIR, fileName);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content);
|
||||
}
|
||||
|
||||
describe('MigrationTimestampRule', () => {
|
||||
let tmpDir: string;
|
||||
let rule: MigrationTimestampRule;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempDir();
|
||||
rule = new MigrationTimestampRule();
|
||||
rule.configure({ options: { now: NOW } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function context(): CodeHealthContext {
|
||||
return { rootDir: tmpDir };
|
||||
}
|
||||
|
||||
it('accepts a precise past timestamp', async () => {
|
||||
writeMigration(tmpDir, '1761047826451-AddWorkflowVersionColumn.ts');
|
||||
|
||||
const violations = await rule.analyze(context());
|
||||
|
||||
expect(violations).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('accepts a rounded past timestamp', async () => {
|
||||
writeMigration(tmpDir, '1766500000000-ExpandInsightsWorkflowIdLength.ts');
|
||||
|
||||
const violations = await rule.analyze(context());
|
||||
|
||||
expect(violations).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('flags a future timestamp', async () => {
|
||||
writeMigration(tmpDir, '1784000000000-AiHallucinatedMigration.ts');
|
||||
|
||||
const violations = await rule.analyze(context());
|
||||
|
||||
expect(violations).toHaveLength(1);
|
||||
expect(violations[0].rule).toBe('migration-timestamp');
|
||||
expect(violations[0].message).toContain('future timestamp');
|
||||
expect(violations[0].message).toContain('1784000000000');
|
||||
});
|
||||
|
||||
it('ignores index.ts and other non-migration files', async () => {
|
||||
writeMigration(tmpDir, 'index.ts');
|
||||
writeMigration(tmpDir, 'README.md');
|
||||
|
||||
const violations = await rule.analyze(context());
|
||||
|
||||
expect(violations).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('scans every configured migration directory', async () => {
|
||||
writeMigration(tmpDir, '1784000000000-Postgres.ts');
|
||||
const sqlitePath = path.join(
|
||||
tmpDir,
|
||||
'packages',
|
||||
'@n8n',
|
||||
'db',
|
||||
'src',
|
||||
'migrations',
|
||||
'sqlite',
|
||||
'1784000000001-Sqlite.ts',
|
||||
);
|
||||
fs.mkdirSync(path.dirname(sqlitePath), { recursive: true });
|
||||
fs.writeFileSync(sqlitePath, 'export {};\n');
|
||||
|
||||
const violations = await rule.analyze(context());
|
||||
|
||||
expect(violations).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { BaseRule } from '@n8n/rules-engine';
|
||||
import type { Violation } from '@n8n/rules-engine';
|
||||
import fg from 'fast-glob';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import type { CodeHealthContext } from '../context.js';
|
||||
|
||||
const DEFAULT_MIGRATION_GLOBS = [
|
||||
'packages/@n8n/db/src/migrations/common/*.ts',
|
||||
'packages/@n8n/db/src/migrations/postgresdb/*.ts',
|
||||
'packages/@n8n/db/src/migrations/sqlite/*.ts',
|
||||
];
|
||||
|
||||
const MIGRATION_FILENAME = /^(\d{10,16})-.+\.ts$/;
|
||||
|
||||
export class MigrationTimestampRule extends BaseRule<CodeHealthContext> {
|
||||
readonly id = 'migration-timestamp';
|
||||
readonly name = 'Migration Timestamp Hygiene';
|
||||
readonly description =
|
||||
'Migration filenames must be prefixed with a Date.now() value from the past — not a future timestamp.';
|
||||
readonly severity = 'error' as const;
|
||||
|
||||
async analyze(context: CodeHealthContext): Promise<Violation[]> {
|
||||
const { rootDir } = context;
|
||||
const options = this.getOptions();
|
||||
|
||||
const globs = Array.isArray(options.migrationGlobs)
|
||||
? (options.migrationGlobs as string[])
|
||||
: DEFAULT_MIGRATION_GLOBS;
|
||||
const now = typeof options.now === 'number' ? options.now : Date.now();
|
||||
|
||||
const files = await fg(globs, { cwd: rootDir, absolute: true });
|
||||
|
||||
const violations: Violation[] = [];
|
||||
for (const filePath of files) {
|
||||
const fileName = path.basename(filePath);
|
||||
const match = MIGRATION_FILENAME.exec(fileName);
|
||||
if (!match) continue;
|
||||
|
||||
const timestamp = Number(match[1]);
|
||||
if (!Number.isFinite(timestamp)) continue;
|
||||
if (timestamp <= now) continue;
|
||||
|
||||
violations.push(
|
||||
this.createViolation(
|
||||
filePath,
|
||||
1,
|
||||
1,
|
||||
`${fileName} is prefixed with a future timestamp (${timestamp}, ${formatDelta(timestamp - now)} ahead of now). Migration prefixes must be the exact Date.now() value at the time of creation — AI agents commonly fabricate future timestamps.`,
|
||||
"Rename the file using the current millisecond timestamp from Date.now() (or 'date +%s%3N').",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDelta(ms: number): string {
|
||||
const abs = Math.abs(ms);
|
||||
const days = Math.floor(abs / 86_400_000);
|
||||
if (days >= 1) return `~${days}d`;
|
||||
const hours = Math.floor(abs / 3_600_000);
|
||||
if (hours >= 1) return `~${hours}h`;
|
||||
const minutes = Math.floor(abs / 60_000);
|
||||
return `~${minutes}m`;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user