diff --git a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts index b758423d..f3233b49 100644 --- a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts +++ b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts @@ -78,6 +78,25 @@ export interface GatewayProcessConfig { baseEnv?: Record; } +const PROFILE_BUILD_ENTRYPOINTS = [ + ['app', 'game-api', 'dist', 'index.js'], + ['app', 'gateway-api', 'dist', 'index.js'], +] as const; + +export const hasCompleteProfileBuildArtifacts = async ( + workspaceRoot: string, + access: (target: string) => Promise = fs.access +): Promise => { + for (const segments of PROFILE_BUILD_ENTRYPOINTS) { + try { + await access(path.join(workspaceRoot, ...segments)); + } catch { + return false; + } + } + return true; +}; + export interface GatewayOrchestratorOptions { repository: GatewayProfileRepository; processManager: ProcessManager; @@ -2386,7 +2405,9 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { if (operationId) { await this.appendOperationLog(operationId, 'workspace', `worktree 준비 완료: ${workspace.root}`); } - const activeWorkspaceReusable = canReuseActiveProfileWorkspace(profile, commitSha, workspace); + const activeWorkspaceReusable = + canReuseActiveProfileWorkspace(profile, commitSha, workspace) && + (await hasCompleteProfileBuildArtifacts(workspace.root)); if (activeWorkspaceReusable) { if (operationId) { await this.appendOperationLog( @@ -2452,6 +2473,12 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { } catch { throw new Error(`Selected commit does not provide the profile seed CLI: ${sourcePath}`); } + const artifactPath = path.join(workspaceRoot, 'app', 'gateway-api', 'dist', 'index.js'); + try { + await fs.access(artifactPath); + } catch { + throw new Error(`Selected commit did not build the profile seed CLI artifact: ${artifactPath}`); + } } private async runProfileMigration( diff --git a/app/gateway-api/test/orchestratorPlan.test.ts b/app/gateway-api/test/orchestratorPlan.test.ts index cff3997e..8e1c526b 100644 --- a/app/gateway-api/test/orchestratorPlan.test.ts +++ b/app/gateway-api/test/orchestratorPlan.test.ts @@ -9,6 +9,7 @@ import { buildProcessDefinitions, buildSharedProfileFrontendCommands, buildWorkspaceCommands, + hasCompleteProfileBuildArtifacts, planProfileReconcile, resolveProfileArchiveServerName, resolveResetLifecycleStatus, @@ -391,6 +392,26 @@ describe('sanitizeManagedProcessEnv', () => { }); describe('buildWorkspaceCommands', () => { + it('does not reuse a DB-preserving deploy workspace that lacks the profile seed artifact', async () => { + const workspaceRoot = '/srv/sammo/worktrees/0123456789abcdef'; + const gameApiPath = path.join(workspaceRoot, 'app', 'game-api', 'dist', 'index.js'); + const gatewayApiPath = path.join(workspaceRoot, 'app', 'gateway-api', 'dist', 'index.js'); + const deployOnlyArtifacts = new Set([gameApiPath]); + + await expect( + hasCompleteProfileBuildArtifacts(workspaceRoot, async (target) => { + if (!deployOnlyArtifacts.has(target)) throw new Error('missing'); + }) + ).resolves.toBe(false); + + deployOnlyArtifacts.add(gatewayApiPath); + await expect( + hasCompleteProfileBuildArtifacts(workspaceRoot, async (target) => { + if (!deployOnlyArtifacts.has(target)) throw new Error('missing'); + }) + ).resolves.toBe(true); + }); + it('installs and builds runtime dependencies before the profile processes', () => { const workspaceRoot = '/srv/sammo/worktrees/0123456789abcdef'; const commands = buildWorkspaceCommands(workspaceRoot, true, undefined, '/srv/sammo/controller');