ci: Speed up the PR build job and move the E2E matrix off its path (no-changelog) (#39736)

This commit is contained in:
Declan Carroll
2026-09-28 13:16:13 +00:00
committed by GitHub
parent 06c61d9437
commit 41dbfd987b
17 changed files with 367 additions and 133 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
"issues": 152
"issues": 141
}
+1 -1
View File
@@ -960,7 +960,7 @@ from the master ruleset, and delete this section (tracked in DEVP-887).
**`blacksmith-4vcpu-ubuntu-2204`** - Unit tests (parallelized), linting (parallel file processing), typechecking (CPU-intensive), E2E test shards
**`blacksmith-8vcpu-ubuntu-2204`** - Heavy parallel workloads
**`blacksmith-8vcpu-ubuntu-2204`** - Heavy parallel workloads. The PR `install-and-build` job uses it with the default runner provider. Most PR jobs wait for that job, and a cold build keeps all 4 vCPUs of a smaller runner busy.
### Runner Provider Toggle
+57 -25
View File
@@ -11,7 +11,7 @@ concurrency:
jobs:
install-and-build:
name: Install & Build
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-8vcpu-ubuntu-2204' }}
env:
NODE_OPTIONS: '--max-old-space-size=7168'
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -31,12 +31,16 @@ jobs:
db_migrations: ${{ fromJSON(steps.ci-filter.outputs.results)['db-migrations'] == true }}
e2e_performance: ${{ fromJSON(steps.ci-filter.outputs.results)['e2e-performance'] == true }}
engine_e2e: ${{ fromJSON(steps.ci-filter.outputs.results)['engine-e2e'] == true }}
# The main E2E suite runs. Read by e2e-matrix and e2e.
run_e2e: >-
${{ github.repository == 'n8n-io/n8n' && github.event_name != 'merge_group' &&
(fromJSON(steps.ci-filter.outputs.results).runtime == true
|| fromJSON(steps.ci-filter.outputs.results).e2e == true
|| contains(github.event.pull_request.labels.*.name, 'force-e2e')) }}
instance_ai_workflow_eval: ${{ fromJSON(steps.ci-filter.outputs.results)['instance-ai-workflow-eval'] == true }}
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 }}
currents_record_key: ${{ secrets.CURRENTS_RECORD_KEY != '' && 'present' || '' }}
@@ -227,20 +231,6 @@ jobs:
if: fromJSON(steps.ci-filter.outputs.results).ci
run: node scripts/turbo-critical-path.mjs >> "$GITHUB_STEP_SUMMARY"
- name: Generate shard matrix
id: generate-matrix
if: github.event_name != 'merge_group' && (fromJSON(steps.ci-filter.outputs.results).ci || fromJSON(steps.ci-filter.outputs.results).e2e)
env:
CHANGED_FILES: ${{ steps.ci-filter.outputs.changed-files }}
MERGE_BASE: ${{ steps.ci-filter.outputs.merge-base }}
PLAYWRIGHT_GREP_INVERT: ${{ github.event.pull_request.head.repo.fork == true && '@licensed' || '' }}
PLAYWRIGHT_PROJECT: ${{ github.event.pull_request.head.repo.fork == true && 'sqlite:e2e' || 'multi-main:e2e' }}
run: |
FILES_CSV=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//')
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 20 --orchestrate --impact "--project=$PLAYWRIGHT_PROJECT" "--files=$FILES_CSV" "--base=$MERGE_BASE")
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.
# PRs run SQLite plus the primary Postgres leg. The merge queue runs the
# full Postgres range, so every merge still gates on the older majors.
@@ -347,7 +337,7 @@ jobs:
# (each of which invokes prepare-docker internally) short-circuit to a
# cache hit instead of racing to rebuild.
prepare-docker:
name: Prepare Docker
name: Prepare E2E
needs: [install-and-build, format-check]
if: >-
github.repository == 'n8n-io/n8n' &&
@@ -364,25 +354,66 @@ jobs:
branch: ${{ needs.install-and-build.outputs.commit_sha }}
secrets: inherit
# Runs next to prepare-docker, so the matrix is off the path of the jobs that
# wait for install-and-build. Only the e2e job reads it.
e2e-matrix:
name: Prepare E2E / Shard matrix
needs: install-and-build
if: needs.install-and-build.outputs.run_e2e == 'true'
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-2vcpu-ubuntu-2204' }}
outputs:
matrix: ${{ steps.generate-matrix.outputs.matrix }}
skip_tests: ${{ steps.generate-matrix.outputs.skip-tests }}
steps:
- uses: useblacksmith/checkout@bcec731f1eb1367240608d1c889a75b96db6ec53 # v1.5.0
with:
ref: ${{ needs.install-and-build.outputs.commit_sha }}
fetch-depth: 1
# Impact selection reads package.json at the merge base.
- name: Fetch merge base
env:
MERGE_BASE: ${{ needs.install-and-build.outputs.merge_base }}
run: |
if [ -n "$MERGE_BASE" ] && ! git cat-file -e "$MERGE_BASE^{commit}" 2>/dev/null; then
git fetch --no-tags --depth=1 origin "$MERGE_BASE"
fi
- name: Setup Node.js
uses: ./.github/actions/setup-nodejs
with:
build-command: pnpm turbo run build --filter=@n8n/playwright-janitor --filter=@n8n/workflow-sdk...
- name: Generate shard matrix
id: generate-matrix
env:
CHANGED_FILES: ${{ needs.install-and-build.outputs.changed_files }}
MERGE_BASE: ${{ needs.install-and-build.outputs.merge_base }}
PLAYWRIGHT_GREP_INVERT: ${{ github.event.pull_request.head.repo.fork == true && '@licensed' || '' }}
PLAYWRIGHT_PROJECT: ${{ github.event.pull_request.head.repo.fork == true && 'sqlite:e2e' || 'multi-main:e2e' }}
run: |
FILES_CSV=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//')
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 20 --orchestrate --impact "--project=$PLAYWRIGHT_PROJECT" "--files=$FILES_CSV" "--base=$MERGE_BASE")
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"
# Full e2e run. Internal PRs run multi-main (postgres + redis + caddy + 2 mains + 1 worker).
# Fork PRs run sqlite-only and skip @licensed tests (no enterprise license secrets on forks).
e2e:
name: E2E
needs: [install-and-build, prepare-docker]
needs: [install-and-build, prepare-docker, e2e-matrix]
if: >-
needs.prepare-docker.result == 'success' &&
(needs.install-and-build.outputs.runtime == 'true'
|| needs.install-and-build.outputs.e2e == 'true'
|| contains(github.event.pull_request.labels.*.name, 'force-e2e')) &&
needs.install-and-build.outputs.skip_tests != 'true' &&
github.event_name != 'merge_group'
needs.e2e-matrix.result == 'success' &&
needs.install-and-build.outputs.run_e2e == 'true' &&
needs.e2e-matrix.outputs.skip_tests != 'true'
uses: ./.github/workflows/test-e2e-reusable.yml
with:
branch: ${{ needs.install-and-build.outputs.commit_sha }}
test-mode: docker-artifact
test-command: ${{ github.event.pull_request.head.repo.fork == true && 'pnpm --filter=n8n-playwright test:container:sqlite:e2e --grep-invert=@licensed' || 'pnpm --filter=n8n-playwright test:container:multi-main:e2e' }}
workers: '1'
pre-generated-matrix: ${{ needs.install-and-build.outputs.matrix }}
pre-generated-matrix: ${{ needs.e2e-matrix.outputs.matrix }}
artifact-prefix: e2e
currents-record-key: ${{ needs.install-and-build.outputs.currents_record_key }}
secrets: inherit
@@ -510,6 +541,7 @@ jobs:
typecheck,
lint,
check-packaging,
e2e-matrix,
e2e,
e2e-engine,
dev-server-smoke,
+1 -1
View File
@@ -117,7 +117,7 @@
"stylelint": "catalog:",
"tsc-alias": "^1.8.10",
"tsc-watch": "^6.2.0",
"turbo": "2.9.15",
"turbo": "2.11.4",
"typescript": "6.0.2",
"yaml": "catalog:",
"zx": "^8.8.5"
+8
View File
@@ -41,6 +41,11 @@
"require": "./dist/index.js",
"import": "./dist/index.js"
},
"./errors": {
"types": "./dist/errors/index.d.ts",
"require": "./dist/errors/index.js",
"import": "./dist/errors/index.js"
},
"./parsers": {
"require": "./dist/parsers/index.js",
"import": "./dist/parsers/index.js",
@@ -55,6 +60,9 @@
},
"typesVersions": {
"*": {
"errors": [
"dist/errors/index.d.ts"
],
"parsers": [
"dist/parsers/index.d.ts"
],
@@ -0,0 +1,6 @@
// A light entry point for callers that need only the error classes, without the
// rest of the package behind the root barrel.
export { FolderResolutionError } from './folder-resolution.error';
export { WorkflowEditorLockedError } from './workflow-editor-locked.error';
export { WorkflowNotFoundError } from './workflow-not-found.error';
export { WorkflowSaveConflictError } from './workflow-save-conflict.error';
+6 -4
View File
@@ -203,10 +203,12 @@ export { parseModelHeadersJson } from './utils/parse-model-headers';
export { modelConfigId } from './utils/model-config-id';
export { isEndpointModelConfig } from './utils/modal-session';
export { resolveCustomModelExperimentDefaultsFromEnv } from './utils/custom-model-defaults';
export { WorkflowSaveConflictError } from './errors/workflow-save-conflict.error';
export { WorkflowNotFoundError } from './errors/workflow-not-found.error';
export { WorkflowEditorLockedError } from './errors/workflow-editor-locked.error';
export { FolderResolutionError } from './errors/folder-resolution.error';
export {
WorkflowSaveConflictError,
WorkflowNotFoundError,
WorkflowEditorLockedError,
FolderResolutionError,
} from './errors';
export {
LEGACY_PLANNED_TASK_KINDS,
PLANNED_TASK_KINDS,
@@ -584,6 +584,96 @@ function findNestedSchemaDir(dir: string, targetNames: string[]): string | undef
return undefined;
}
/**
* Folder name (lowercased) to `__schema__` path for one schema root. It holds the
* same answers as the direct search in `findSchemaDirectory`, but it reads each
* directory once.
*/
interface SchemaRootIndex {
/** Direct children of the root. The flat search checks these first. */
flat: Map<string, string>;
/** First match in the pre-order walk that `findNestedSchemaDir` does. */
nested: Map<string, string>;
}
function buildSchemaRootIndex(root: string): SchemaRootIndex {
const index: SchemaRootIndex = { flat: new Map(), nested: new Map() };
const walk = (dir: string, depth: number) => {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const entryPath = path.join(dir, entry.name);
const key = entry.name.toLowerCase();
const schemaPath = path.join(entryPath, '__schema__');
if (fs.existsSync(schemaPath)) {
if (depth === 0 && !index.flat.has(key)) index.flat.set(key, schemaPath);
if (!index.nested.has(key)) index.nested.set(key, schemaPath);
}
if (entry.name !== '__schema__' && entry.name !== 'node_modules') {
walk(entryPath, depth + 1);
}
}
};
walk(root, 0);
return index;
}
/**
* Schema root indexes for the active generation run. A run looks up hundreds of
* node versions, and each miss used to walk the whole `dist/nodes` tree again.
* The indexes live only for one run, because other callers (and tests) can add
* schema directories between calls.
*/
let activeSchemaIndexes: Map<string, SchemaRootIndex> | undefined;
/** Run `fn` with schema directory lookups served from a one-time index of each root. */
export async function withSchemaDirIndex<T>(fn: () => Promise<T>): Promise<T> {
if (activeSchemaIndexes) return await fn();
activeSchemaIndexes = new Map();
try {
return await fn();
} finally {
activeSchemaIndexes = undefined;
}
}
function indexedSchemaDirectory(
indexes: Map<string, SchemaRootIndex>,
roots: string[],
baseName: string,
): string | undefined {
const rootIndexes = roots.map((root) => {
let index = indexes.get(root);
if (!index) {
index = buildSchemaRootIndex(root);
indexes.set(root, index);
}
return index;
});
const key = baseName.toLowerCase();
for (const index of rootIndexes) {
const found = index.flat.get(key);
if (found) return found;
}
for (const index of rootIndexes) {
const found = index.nested.get(key);
if (found) return found;
}
return undefined;
}
/**
* Schema roots to search, in priority order: the package currently being
* generated (generate-node-defs-cli runs with CWD = that package, so e.g.
@@ -604,7 +694,7 @@ export function schemaSearchRoots(): string[] {
* @param baseName The base node name (e.g., 'gmail')
* @returns Path to the __schema__ directory, or undefined if not found
*/
function findSchemaDirectory(baseName: string, schemaPath?: string): string | undefined {
export function findSchemaDirectory(baseName: string, schemaPath?: string): string | undefined {
const roots = schemaSearchRoots();
// If explicit schemaPath is provided, use it directly
@@ -617,6 +707,10 @@ function findSchemaDirectory(baseName: string, schemaPath?: string): string | un
}
}
if (activeSchemaIndexes) {
return indexedSchemaDirectory(activeSchemaIndexes, roots, baseName);
}
const possibleNames = [
baseName.charAt(0).toUpperCase() + baseName.slice(1), // Gmail
baseName, // gmail
@@ -4611,6 +4705,10 @@ export interface GenerationResult {
* @returns Result with count of nodes processed
*/
export async function orchestrateGeneration(options: GenerationOptions): Promise<GenerationResult> {
return await withSchemaDirIndex(async () => await generateAll(options));
}
async function generateAll(options: GenerationOptions): Promise<GenerationResult> {
const { nodes, outputDir } = options;
// Group nodes by package, filtering hidden
@@ -0,0 +1,98 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { findSchemaDirectory, withSchemaDirIndex } from './generate-types';
function addSchemaDir(root: string, relativeDir: string): string {
const schemaDir = path.join(root, relativeDir, '__schema__');
fs.mkdirSync(path.join(schemaDir, 'v1.0.0'), { recursive: true });
fs.writeFileSync(path.join(schemaDir, 'v1.0.0', 'output.json'), '{}');
return schemaDir;
}
describe('schema directory index', () => {
let packageDir: string;
let nodesRoot: string;
let originalCwd: string;
beforeEach(() => {
originalCwd = process.cwd();
// realpath: on macOS the temp dir is a symlink, and process.cwd() resolves it.
packageDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'schema-dir-index-test-')));
nodesRoot = path.join(packageDir, 'dist', 'nodes');
fs.mkdirSync(nodesRoot, { recursive: true });
// schemaSearchRoots() puts <cwd>/dist/nodes ahead of nodes-base.
process.chdir(packageDir);
});
afterEach(() => {
process.chdir(originalCwd);
fs.rmSync(packageDir, { recursive: true, force: true });
});
const names = [
'__IdxTarget__',
'__idxchainllm__',
'__IdxSheets__',
'__IdxDuplicate__',
'__IdxHidden__',
'__IdxMissing__',
];
function buildTree() {
// A nested match that sorts before the flat match. The flat match must win.
addSchemaDir(nodesRoot, 'AAA/__IdxTarget__');
const flatTarget = addSchemaDir(nodesRoot, '__IdxTarget__');
// Folder casing differs from the node name.
const chain = addSchemaDir(nodesRoot, 'chains/__IDXCHAINLLM__');
// A folder with the node name but no schema, with a match below it.
fs.mkdirSync(path.join(nodesRoot, '__IdxSheets__'), { recursive: true });
const sheets = addSchemaDir(nodesRoot, '__IdxSheets__/V2/__IdxSheets__');
// Two nested matches. Walk order decides, the same as the direct search.
addSchemaDir(nodesRoot, 'Group1/__IdxDuplicate__');
addSchemaDir(nodesRoot, 'Group2/__IdxDuplicate__');
// The search skips node_modules.
addSchemaDir(nodesRoot, 'node_modules/__IdxHidden__');
return { flatTarget, chain, sheets };
}
it('returns the same directories as the direct search', async () => {
const { flatTarget, chain, sheets } = buildTree();
const direct = names.map((name) => findSchemaDirectory(name));
const indexed = await withSchemaDirIndex(
async () => await Promise.resolve(names.map((name) => findSchemaDirectory(name))),
);
expect(indexed).toEqual(direct);
expect(indexed[0]).toBe(flatTarget);
expect(indexed[1]).toBe(chain);
expect(indexed[2]).toBe(sheets);
expect(indexed[3]).toBeDefined();
expect(indexed[4]).toBeUndefined();
expect(indexed[5]).toBeUndefined();
});
it('keeps an explicit schema path ahead of the name lookup', async () => {
buildTree();
const explicit = addSchemaDir(nodesRoot, 'custom/place');
const result = await withSchemaDirIndex(
async () => await Promise.resolve(findSchemaDirectory('__IdxTarget__', 'custom/place')),
);
expect(result).toBe(explicit);
});
it('does not keep the index after the run ends', async () => {
await withSchemaDirIndex(async () => await Promise.resolve(findSchemaDirectory('__IdxLate__')));
const late = addSchemaDir(nodesRoot, '__IdxLate__');
expect(findSchemaDirectory('__IdxLate__')).toBe(late);
const nextRun = await withSchemaDirIndex(
async () => await Promise.resolve(findSchemaDirectory('__IdxLate__')),
);
expect(nextRun).toBe(late);
});
});
+41 -15
View File
@@ -34,14 +34,26 @@ function generateUserManagementEmailTemplates() {
shell.mkdir('-p', destinationDir);
const templates = glob.sync('*.mjml', { cwd: sourceDir });
templates.forEach((template) => {
if (template.startsWith('_')) return;
const source = path.resolve(sourceDir, template);
const destination = path.resolve(destinationDir, template.replace(/\.mjml$/, '.handlebars'));
const command = `pnpm mjml --output "${destination}" "${source}"`;
shell.exec(command, { silent: false });
const templates = glob.sync('*.mjml', { cwd: sourceDir }).filter((t) => !t.startsWith('_'));
// One mjml process for all templates: each `pnpm mjml` start costs about 0.3s.
// With several inputs, mjml writes `<name>.html` into the output directory.
const sources = templates.map((template) => `"${path.resolve(sourceDir, template)}"`).join(' ');
const result = shell.exec(`pnpm mjml ${sources} --output "${destinationDir}${path.sep}"`, {
silent: false,
});
if (result.code !== 0) {
throw new Error(`mjml failed to compile the email templates (exit code ${result.code})`);
}
for (const template of templates) {
const name = template.replace(/\.mjml$/, '');
const compiled = path.resolve(destinationDir, `${name}.html`);
if (!existsSync(compiled)) {
throw new Error(`mjml did not write ${compiled}`);
}
shell.mv(compiled, path.resolve(destinationDir, `${name}.handlebars`));
}
shell.cp(path.resolve(sourceDir, 'n8n-logo.png'), destinationDir);
}
@@ -74,14 +86,16 @@ async function buildPublicApiSpec() {
// 1. Generate decorated route OpenAPI specs from source.
generateDocs(v1Dir);
// 2. Bundle both sources.
// 2. Bundle both sources in one redocly process. With several inputs, redocly
// writes each bundle into the output directory under its source file name.
const v1DistDir = path.resolve(ROOT_DIR, 'dist', 'public-api', 'v1');
bundleSpecs(
[path.join(v1Dir, SPEC_FILENAME), path.join(v1Dir, DECORATOR_ROOT_FILENAME)],
v1DistDir,
);
const eovDistSpec = path.join(v1DistDir, SPEC_FILENAME);
bundleSpec(path.join(v1Dir, SPEC_FILENAME), eovDistSpec);
const decoratorDistSpec = path.join(v1DistDir, DECORATOR_ROOT_FILENAME);
bundleSpec(path.join(v1Dir, DECORATOR_ROOT_FILENAME), decoratorDistSpec);
// 3. Merge the two specs into a single OpenAPI document, writing back to the eov spec path.
const eovDoc = parseYaml(readFileSync(eovDistSpec, 'utf8'));
@@ -125,13 +139,25 @@ async function loadOpenApiGenerator() {
return generator;
}
// Bundles a spec through redocly, resolving all $refs into a single file at `distPath`.
function bundleSpec(sourcePath, distPath) {
const result = shell.exec(`pnpm openapi bundle "${sourcePath}" --output "${distPath}"`, {
// Bundles specs through redocly, resolving all $refs. Each source becomes one file in `distDir`.
function bundleSpecs(sourcePaths, distDir) {
const distPaths = sourcePaths.map((sourcePath) => path.join(distDir, path.basename(sourcePath)));
// Remove old output, so that the check below cannot pass on a stale file.
for (const distPath of distPaths) rmSync(distPath, { force: true });
const sources = sourcePaths.map((sourcePath) => `"${sourcePath}"`).join(' ');
const result = shell.exec(`pnpm openapi bundle ${sources} --output "${distDir}"`, {
silent: true,
});
if (result.code !== 0) {
throw new Error(`redocly failed to bundle ${sourcePath}:\n${result.stderr || result.stdout}`);
throw new Error(
`redocly failed to bundle ${sourcePaths.join(', ')}:\n${result.stderr || result.stdout}`,
);
}
for (const [i, distPath] of distPaths.entries()) {
if (!existsSync(distPath)) {
throw new Error(`redocly did not write the bundle for ${sourcePaths[i]} to ${distPath}`);
}
}
}
@@ -1,24 +1,5 @@
import type { InstanceAiAgentNode, InstanceAiEvent } from '@n8n/api-types';
// `@n8n/instance-ai` source util imported dynamically (no built entry for this
// deep subpath); loaded in `beforeAll` to avoid top-level `await`.
let buildAgentTreeFromEvents: (events: InstanceAiEvent[]) => InstanceAiAgentNode;
let findAgentNodeInTree: (
tree: InstanceAiAgentNode,
agentId: string,
) => InstanceAiAgentNode | undefined;
beforeAll(async () => {
({ buildAgentTreeFromEvents, findAgentNodeInTree } = (await import(
'../../../../../@n8n/instance-ai/src/utils/agent-tree.js'
)) as {
buildAgentTreeFromEvents: (events: InstanceAiEvent[]) => InstanceAiAgentNode;
findAgentNodeInTree: (
tree: InstanceAiAgentNode,
agentId: string,
) => InstanceAiAgentNode | undefined;
});
});
import type { InstanceAiEvent } from '@n8n/api-types';
import { buildAgentTreeFromEvents, findAgentNodeInTree } from '@n8n/instance-ai';
describe('buildAgentTreeFromEvents', () => {
it('should build a tree from run-start + text-delta + run-finish', () => {
@@ -1,15 +1,8 @@
// The barrel is mocked so this file exercises the adapter's own wiring rather than pulling the
// whole instance-ai package in behind it, matching the other adapter tests.
vi.mock('@n8n/instance-ai', async () => {
const { WorkflowSaveConflictError } = await import(
'../../../../../@n8n/instance-ai/src/errors/workflow-save-conflict.error.js'
);
const { WorkflowNotFoundError } = await import(
'../../../../../@n8n/instance-ai/src/errors/workflow-not-found.error.js'
);
const { WorkflowEditorLockedError } = await import(
'../../../../../@n8n/instance-ai/src/errors/workflow-editor-locked.error.js'
);
const { WorkflowSaveConflictError, WorkflowNotFoundError, WorkflowEditorLockedError } =
await import('@n8n/instance-ai/errors');
return {
WorkflowSaveConflictError,
WorkflowNotFoundError,
@@ -1,8 +1,6 @@
// Mock the barrel import so these adapter tests only exercise local formatting helpers.
vi.mock('@n8n/instance-ai', async () => {
const { WorkflowNotFoundError } = await import(
'../../../../../@n8n/instance-ai/src/errors/workflow-not-found.error.js'
);
const { WorkflowNotFoundError } = await import('@n8n/instance-ai/errors');
return {
WorkflowNotFoundError,
wrapUntrustedData(content: string, source: string, label?: string): string {
@@ -1,17 +1,11 @@
// Mock the barrel import so these adapter tests only exercise local formatting helpers.
vi.mock('@n8n/instance-ai', async () => {
const { WorkflowSaveConflictError } = await import(
'../../../../../@n8n/instance-ai/src/errors/workflow-save-conflict.error.js'
);
const { WorkflowNotFoundError } = await import(
'../../../../../@n8n/instance-ai/src/errors/workflow-not-found.error.js'
);
const { WorkflowEditorLockedError } = await import(
'../../../../../@n8n/instance-ai/src/errors/workflow-editor-locked.error.js'
);
const { FolderResolutionError } = await import(
'../../../../../@n8n/instance-ai/src/errors/folder-resolution.error.js'
);
const {
WorkflowSaveConflictError,
WorkflowNotFoundError,
WorkflowEditorLockedError,
FolderResolutionError,
} = await import('@n8n/instance-ai/errors');
return {
WorkflowSaveConflictError,
WorkflowNotFoundError,
@@ -1814,9 +1808,11 @@ import type { DataTableRepository } from '@/modules/data-table/data-table.reposi
import type { DataTableService } from '@/modules/data-table/data-table.service';
import type { InstanceWriteAccessService } from '@/services/instance-write-access.service';
import type { WorkflowJSON } from '@n8n/workflow-sdk';
import { WorkflowEditorLockedError } from '../../../../../@n8n/instance-ai/src/errors/workflow-editor-locked.error';
import { WorkflowNotFoundError } from '../../../../../@n8n/instance-ai/src/errors/workflow-not-found.error';
import { WorkflowSaveConflictError } from '../../../../../@n8n/instance-ai/src/errors/workflow-save-conflict.error';
import {
WorkflowEditorLockedError,
WorkflowNotFoundError,
WorkflowSaveConflictError,
} from '@n8n/instance-ai/errors';
import type { WorkflowService } from '@/workflows/workflow.service';
import { AiPreferenceScopeFullError } from '@/errors/response-errors/ai-preference-scope-full.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
-2
View File
@@ -13,8 +13,6 @@
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
"typecheck": "tsc --noEmit",
"build": "tsc --noEmit",
"build:unchecked": "pnpm run build",
"build-node-dev": "tsc",
"format": "biome format --write .",
"format:check": "biome ci .",
+31 -31
View File
@@ -1141,8 +1141,8 @@ importers:
specifier: ^6.2.0
version: 6.2.0([email protected])
turbo:
specifier: 2.9.15
version: 2.9.15
specifier: 2.11.4
version: 2.11.4
typescript:
specifier: 6.0.2
version: 6.0.2
@@ -14060,33 +14060,33 @@ packages:
'@tsconfig/[email protected]':
resolution: {integrity: sha512-T/CfdwFry660WjZor56z0F3pxeCllt8KOxWcHFW6ZEuULKUObTDEMdgtctyuJPxwqyWDsvHRfxHaJ4FIICyoqQ==}
'@turbo/darwin-64@2.9.18':
resolution: {integrity: sha512-9f27peFu16ur8c0v9nUFUEyBnbKuuFsUTjHFWfmwGfzySBXbHwzU44QhZon6Mznz0cHsIr3984NQj/bVrnGSRw==}
'@turbo/darwin-64@2.11.4':
resolution: {integrity: sha512-fEmcQVa95k79tZjcay5b2A4cpR/VrlNUki4AN7EbA+32vj5C5rBUE1q3i2yOFxsB2n01sOlzIZ+s2p2zADCTig==}
cpu: [x64]
os: [darwin]
'@turbo/darwin-arm64@2.9.15':
resolution: {integrity: sha512-fDSx56oqoFuS+yUQw7hqjQTkjrSLdMcplhuLC8HcSkWC6YrpwEmUUYsPYHPxy4ALvLxnmPQuk6XoSD8tdkjP+g==}
'@turbo/darwin-arm64@2.11.4':
resolution: {integrity: sha512-SgXCVgBC1v+bdE6+KQGV6Kob7nBYo75zxzNFFuA4jLdM0J8lYamdTUoZSHLR0dy+kXVEYZqSC98CKqC17NG3Og==}
cpu: [arm64]
os: [darwin]
'@turbo/linux-64@2.9.18':
resolution: {integrity: sha512-zCdIDtz69AnbYh913elJRRoF3QY5aa2HNnf+4rAkc7bQ+tWujiDkCNV7stazOUPggaDvhKIf2Z87qHftTeXSkw==}
'@turbo/linux-64@2.11.4':
resolution: {integrity: sha512-MWM7Au6NPJMBmHJAzFzQ/PACYNRpBOqnBkj+7csdR+YhJ+0XBg4Z64wFwLnGFDac0gc+YlasWOZ5t05bGs54aQ==}
cpu: [x64]
os: [linux]
os: [android, linux]
'@turbo/linux-arm64@2.9.15':
resolution: {integrity: sha512-cbOaDe1ijz5As+mimOOHgmRMolZZZO7miNBHHp5xdiYMm2Q/Dwu1JVLx/Kw4s7xjocG/oEoHrpHrxpEAIEfNiw==}
'@turbo/linux-arm64@2.11.4':
resolution: {integrity: sha512-rRVOh0UNlLbKPuf70FokqjahO/hCGgBmgvZcNXUPayn8Wk0zXIsVoNKpr0UQUhhR+KMiibvBKkVAGDViDFQHKA==}
cpu: [arm64]
os: [linux]
os: [android, linux]
'@turbo/windows-64@2.9.18':
resolution: {integrity: sha512-m0kDhZANxSNz9ck1ybogFscHabriAsp4eDFNrN/1H5WrgTF7b3VlcPZnhuO3v2+E2KnCbeAc+UUT10BZZHdDKw==}
'@turbo/windows-64@2.11.4':
resolution: {integrity: sha512-COr+++1GwDg09XbJdm3sP1gEQApeV43UknYP9h82qet1tAGJrGQAxKVTMrf2JMsMw8MkJRnQj3gAVuQmV1kIyQ==}
cpu: [x64]
os: [win32]
'@turbo/windows-arm64@2.9.15':
resolution: {integrity: sha512-fOHEsLcqVdFXLw2ApWv4gxwfHzkUnpo9rHGml+9+dyHj148m/Bc+556kEvb5+4u6prI1LMd8zEZE2HcO6Jn2VQ==}
'@turbo/windows-arm64@2.11.4':
resolution: {integrity: sha512-gh5lwDA2d4aBdLvTANhFsFEgxbBqKZ4m4dCyChw9GoX8b+Esk8H3p25Q+rlWCUVJ2o6Hne10ZTL0QSFfWD4gIA==}
cpu: [arm64]
os: [win32]
@@ -23347,8 +23347,8 @@ packages:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
turbo@2.9.15:
resolution: {integrity: sha512-VpKvD9Z0Hu/xrGUAYX1wnhfpqv835wIwGqeKfulvBPTOcDap0E3nFwyzCAVV85fB1sBcBDEfTP+7FSW7GzwWSQ==}
turbo@2.11.4:
resolution: {integrity: sha512-nedZtJ2hjWpU/JBFq7Aj1GQBxQ56si+kIUmNZBUX35O35TIuhSBR6/u3yHHmk81U+RBdBcVjvLQ3/iOFzMwKsA==}
hasBin: true
[email protected]:
@@ -32115,22 +32115,22 @@ snapshots:
'@tsconfig/[email protected]': {}
'@turbo/darwin-64@2.9.18':
'@turbo/darwin-64@2.11.4':
optional: true
'@turbo/darwin-arm64@2.9.15':
'@turbo/darwin-arm64@2.11.4':
optional: true
'@turbo/linux-64@2.9.18':
'@turbo/linux-64@2.11.4':
optional: true
'@turbo/linux-arm64@2.9.15':
'@turbo/linux-arm64@2.11.4':
optional: true
'@turbo/windows-64@2.9.18':
'@turbo/windows-64@2.11.4':
optional: true
'@turbo/windows-arm64@2.9.15':
'@turbo/windows-arm64@2.11.4':
optional: true
'@tybys/[email protected]':
@@ -43527,14 +43527,14 @@ snapshots:
[email protected]: {}
turbo@2.9.15:
turbo@2.11.4:
optionalDependencies:
'@turbo/darwin-64': 2.9.18
'@turbo/darwin-arm64': 2.9.15
'@turbo/linux-64': 2.9.18
'@turbo/linux-arm64': 2.9.15
'@turbo/windows-64': 2.9.18
'@turbo/windows-arm64': 2.9.15
'@turbo/darwin-64': 2.11.4
'@turbo/darwin-arm64': 2.11.4
'@turbo/linux-64': 2.11.4
'@turbo/linux-arm64': 2.11.4
'@turbo/windows-64': 2.11.4
'@turbo/windows-arm64': 2.11.4
[email protected]:
dependencies:
+2 -4
View File
@@ -11,10 +11,8 @@
* fewer issues. Nothing raises it automatically: a count above the baseline
* fails the nightly instead.
*
* The baseline is the POST-BUILD count: CI's lint job builds dependencies before
* this check runs, and built dist trees surface ~8 extra issues that a cold
* checkout doesn't. Run `pnpm build` before `pnpm boundaries:baseline`, or CI
* will read a higher count than you saw locally.
* Turbo 2.11 skips gitignored files, so built dist trees no longer change the
* count. It also checks dynamic `import()` calls, which 2.9 did not.
*
* ponytail: parses turbo's "N issues found" line — the ratchet is a single
* number, not a per-issue snapshot, so it can't tell a fixed issue from a new