From 57112cdecbc167a2a484897aaf5f797bd9bca2fa Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 13 Apr 2026 18:49:37 +0200 Subject: [PATCH] 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. --- ...-17-migrate-workflow-code-steps.command.ts | 91 +++++++++++++++---- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command.ts index caec666f9a5..daf50b722bb 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command.ts @@ -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 { + 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,