Fix 1.17 upgrade: build from source when built-function folder is missing

v1.17.3 swallowed the error and skipped the function entirely,
meaning never-executed serverless functions were silently dropped
during migration.

Instead, when the built folder is missing, build the source
TypeScript on the fly using esbuild (same config used by
LogicFunctionResourceService) so the migration completes properly.
This commit is contained in:
Charles Bochet
2026-04-13 18:49:37 +02:00
parent 1d84e23a1b
commit 57112cdecb
@@ -2,8 +2,13 @@ import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import crypto from 'crypto';
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { build } from 'esbuild';
import { Command } from 'nest-commander';
import { NODE_ESM_CJS_BANNER } from 'twenty-shared/application';
import { FileFolder, type Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
@@ -245,32 +250,86 @@ export class MigrateWorkflowCodeStepsCommand extends ActiveOrSuspendedWorkspaces
): Promise<{ builtContent: string; sourceContent: string } | null> {
const workspacePrefix = `workspace-${workspaceId}`;
const builtSources = await this.fileStorageService.readFolderLegacy(
`${workspacePrefix}/${OLD_BUILT_FOLDER}/${serverlessFunctionId}/${version}`,
);
let builtSources: Sources | null = null;
const sourceSources = await this.fileStorageService.readFolderLegacy(
`${workspacePrefix}/${OLD_SOURCE_FOLDER}/${serverlessFunctionId}/${version}`,
);
// Old source layout may nest files under a `src/` key
const sourceRoot =
(sourceSources.src as Sources) ?? (sourceSources as Sources);
const builtContent = builtSources['index.mjs'] as string;
const sourceContent = sourceRoot['index.ts'] as string;
if (!isDefined(builtContent) || !isDefined(sourceContent)) {
try {
builtSources = await this.fileStorageService.readFolderLegacy(
`${workspacePrefix}/${OLD_BUILT_FOLDER}/${serverlessFunctionId}/${version}`,
);
} catch {
this.logger.warn(
`Missing index.mjs or index.ts for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, skipping`,
`Built folder not found for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, will build from source`,
);
}
let sourceSources: Sources;
try {
sourceSources = await this.fileStorageService.readFolderLegacy(
`${workspacePrefix}/${OLD_SOURCE_FOLDER}/${serverlessFunctionId}/${version}`,
);
} catch {
this.logger.warn(
`Source folder not found for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, skipping`,
);
return null;
}
// Old source layout may nest files under a `src/` key
const sourceRoot =
(sourceSources.src as Sources) ?? (sourceSources as Sources);
const sourceContent = sourceRoot['index.ts'] as string;
if (!isDefined(sourceContent)) {
this.logger.warn(
`Missing index.ts for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, skipping`,
);
return null;
}
let builtContent = builtSources?.['index.mjs'] as string | undefined;
if (!isDefined(builtContent)) {
this.logger.log(
`Building serverless function ${serverlessFunctionId}/${version} from source in workspace ${workspaceId}`,
);
builtContent = await this.buildSourceToMjs(sourceContent);
}
return { builtContent, sourceContent };
}
private async buildSourceToMjs(sourceContent: string): Promise<string> {
const tempDir = await mkdtemp(join(tmpdir(), 'twenty-migrate-'));
try {
const srcDir = join(tempDir, 'src');
const outFile = join(tempDir, 'index.mjs');
await mkdir(srcDir, { recursive: true });
await writeFile(join(srcDir, 'index.ts'), sourceContent);
await build({
entryPoints: [join(srcDir, 'index.ts')],
outfile: outFile,
platform: 'node',
format: 'esm',
target: 'es2017',
bundle: true,
sourcemap: true,
packages: 'external',
banner: NODE_ESM_CJS_BANNER,
});
return await readFile(outFile, 'utf-8');
} finally {
await rm(tempDir, { recursive: true, force: true });
}
}
private async uploadFunctionFiles(
workspaceId: string,
applicationUniversalIdentifier: string,