fix(core): Extract workflow-sdk examples to a writable cache dir (#30433)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
José Braulio González Valido 2026-05-14 10:10:23 +01:00 committed by GitHub
parent 563089ba70
commit 6beed60969
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 43 additions and 12 deletions

View File

@ -22,7 +22,7 @@
*/
import type { Workspace } from '@mastra/core/workspace';
import { getExampleFiles } from '@n8n/workflow-sdk/examples-loader';
import { getExampleFiles, type ExampleFile } from '@n8n/workflow-sdk/examples-loader';
import { createRequire } from 'node:module';
import type { Logger } from '../logger';
@ -329,7 +329,17 @@ export async function getWorkspaceRoot(workspace: Workspace): Promise<string> {
*/
export async function writeCuratedExamples(workspace: Workspace, logger?: Logger): Promise<void> {
const start = Date.now();
const { files: exampleFiles, indexTxt } = getExampleFiles();
// Examples are nice-to-have — never block the build when loading them fails.
let exampleFiles: ExampleFile[];
let indexTxt: string;
try {
({ files: exampleFiles, indexTxt } = getExampleFiles());
} catch (error) {
logger?.warn('[sandbox-setup] curated examples unavailable, continuing without', {
error: error instanceof Error ? error.message : String(error),
});
return;
}
if (exampleFiles.length === 0) return;
const root = await getWorkspaceRoot(workspace);

View File

@ -15,14 +15,12 @@ import * as fs from 'fs';
import * as path from 'path';
import { emitInstanceAi } from './codegen/emit-instance-ai';
import { ensureExtracted } from './examples-zip';
import { ensureExtracted, WORKFLOWS_CACHE_DIR } from './examples-zip';
import type { WorkflowJSON } from './types/base';
// Resolve relative to this file. At runtime this lives at <package>/dist/examples-loader.js,
// so `../examples` reaches <package>/examples/.
// Manifest ships read-only in the package; workflows live in WORKFLOWS_CACHE_DIR.
const EXAMPLES_DIR = path.resolve(__dirname, '..', 'examples');
const MANIFEST_PATH = path.join(EXAMPLES_DIR, 'manifest.json');
const WORKFLOWS_DIR = path.join(EXAMPLES_DIR, 'workflows');
const NODES_INLINE_LIMIT = 5;
const INDEX_NODE_SEPARATOR = ',';
@ -95,7 +93,7 @@ function loadFromDisk(): ExampleFilesBundle {
const indexLines: string[] = [];
for (const entry of entries) {
const wfPath = path.join(WORKFLOWS_DIR, `${entry.slug}.json`);
const wfPath = path.join(WORKFLOWS_CACHE_DIR, `${entry.slug}.json`);
if (!fs.existsSync(wfPath)) continue;
// eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Internal workflow fixture
const wf = JSON.parse(fs.readFileSync(wfPath, 'utf-8')) as WorkflowJSON;

View File

@ -8,12 +8,35 @@
*/
import AdmZip from 'adm-zip';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const EXAMPLES_DIR = path.resolve(__dirname, '..', 'examples');
const ZIP_PATH = path.join(EXAMPLES_DIR, 'templates.zip');
const MANIFEST_PATH = path.join(EXAMPLES_DIR, 'manifest.json');
const WORKFLOWS_DIR = path.join(EXAMPLES_DIR, 'workflows');
function sdkVersion(): string {
try {
const pkgPath = path.resolve(__dirname, '..', 'package.json');
// eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Own package.json
return (
(JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version?: string }).version ??
'unversioned'
);
} catch {
return 'unversioned';
}
}
// Tmp cache for unzipped workflows — keyed by SDK version so upgrades extract
// fresh. We can't unzip back into the package because node_modules is
// read-only inside n8n's Docker image.
export const WORKFLOWS_CACHE_DIR = path.join(
os.tmpdir(),
'n8n-workflow-sdk',
sdkVersion(),
'workflows',
);
interface ManifestEntry {
slug: string;
@ -41,7 +64,7 @@ export function needsExtraction(): boolean {
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8')) as ManifestFile;
for (const entry of manifest.workflows ?? []) {
if (!entry.success || entry.skip) continue;
const filePath = path.join(WORKFLOWS_DIR, `${entry.slug}.json`);
const filePath = path.join(WORKFLOWS_CACHE_DIR, `${entry.slug}.json`);
if (!fs.existsSync(filePath)) return true;
}
return false;
@ -55,14 +78,14 @@ export function extractFromZip(): void {
if (!fs.existsSync(ZIP_PATH)) {
throw new Error(`Examples zip not found: ${ZIP_PATH}`);
}
if (!fs.existsSync(WORKFLOWS_DIR)) {
fs.mkdirSync(WORKFLOWS_DIR, { recursive: true });
if (!fs.existsSync(WORKFLOWS_CACHE_DIR)) {
fs.mkdirSync(WORKFLOWS_CACHE_DIR, { recursive: true });
}
const zip = new AdmZip(ZIP_PATH);
for (const entry of zip.getEntries()) {
if (entry.isDirectory) continue;
zip.extractEntryTo(entry, WORKFLOWS_DIR, false, true);
zip.extractEntryTo(entry, WORKFLOWS_CACHE_DIR, false, true);
}
}