fix: Gateway 빌드와 마이그레이션 실행기를 분리

release build는 구성된 격리 builder로 보내고 Gateway DB migration은 runtime의 로컬 runner에서만 실행한다. 원격 build와 로컬 migration 명령 경계를 회귀 테스트로 고정한다.
This commit is contained in:
2026-08-22 10:28:16 +00:00
parent a3092bcfcc
commit 57c1d03ff6
3 changed files with 68 additions and 4 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ const main = async (): Promise<void> => {
const controller = new GatewayReleaseController(
repository,
workspaceManager,
releaseBuildRunner,
migrationRunner,
processManager,
config
);
@@ -146,13 +146,13 @@ export class GatewayReleaseController {
constructor(
private readonly repository: GatewayReleaseRepository,
private readonly workspaceManager: GitWorkspaceManager,
private readonly buildRunner: BuildRunner,
private readonly migrationRunner: BuildRunner,
private readonly processManager: ProcessManager,
private readonly config: ReleaseControllerConfig,
private readonly now: () => Date = () => new Date(),
private readonly fetchImpl: typeof fetch = fetch
) {
this.releaseBuildRunner = createReleaseBuildRunner(config.releaseBuilderUrl, buildRunner, fetchImpl);
this.releaseBuildRunner = createReleaseBuildRunner(config.releaseBuilderUrl, migrationRunner, fetchImpl);
this.artifactManager = new FrontendArtifactManager(config.frontendArtifactRoot ?? '/srv/frontend-artifacts');
}
@@ -345,7 +345,7 @@ export class GatewayReleaseController {
: null;
await this.appendLog(operation.id, 'migration', 'Gateway database migration을 적용합니다.');
await this.assertOperationLease(operation.id);
const migration = await this.buildRunner.run(
const migration = await this.migrationRunner.run(
[buildGatewayMigrationCommand(workspace.root, this.config)],
this.buildProgress(operation.id, 'migration'),
{ signal }
@@ -388,6 +388,70 @@ describe('GatewayReleaseController', () => {
);
});
it('runs release builds remotely while keeping Gateway migration on the runtime runner', async () => {
const workspace = await createReleaseWorkspace();
const harness = createRepository();
const localCommandGroups: string[][] = [];
const remoteRequests: Array<{ commands: Array<{ args: string[] }> }> = [];
const running = new Map(gatewayNames.map((name) => [name, '/srv/sammo/old']));
const localRunner: BuildRunner = {
run: async (commands) => {
localCommandGroups.push(commands.map((command) => command.args.join(' ')));
return { ok: true, exitCode: 0, output: '' };
},
};
const controller = new GatewayReleaseController(
harness.repository,
{
resolveCommit: async () => SHA,
prepare: async () => ({ root: workspace, created: true, needsInstall: true }),
} as unknown as GitWorkspaceManager,
localRunner,
{
list: async () =>
[...running].map(([name, cwd]) => ({
name,
cwd,
status: 'online',
restartCount: 0,
script: path.join(cwd, 'dist.js'),
})),
start: async (definition) => {
running.set(definition.name, definition.cwd);
},
stop: async () => {},
delete: async (name) => {
running.delete(name);
},
},
{ ...config, releaseBuilderUrl: 'http://builder:15100' },
() => new Date('2026-08-01T00:00:00.000Z'),
async (input, init) => {
if (String(input) === 'http://builder:15100/v1/builds') {
remoteRequests.push(JSON.parse(String(init?.body)) as { commands: Array<{ args: string[] }> });
return new Response(`${JSON.stringify({ result: { ok: true, exitCode: 0, output: '' } })}\n`, {
status: 200,
headers: { 'content-type': 'application/x-ndjson' },
});
}
return new Response('', { status: 200 });
}
);
await controller.runOnce();
expect(remoteRequests).toHaveLength(1);
expect(remoteRequests[0]?.commands.map((command) => command.args.join(' '))).toEqual(
expect.arrayContaining([
expect.stringContaining('install --frozen-lockfile'),
expect.stringContaining('turbo run build'),
expect.stringContaining('turbo run build:release'),
])
);
expect(localCommandGroups).toEqual([['--filter @sammo-ts/infra prisma:migrate:deploy:gateway']]);
expect(harness.completions).toEqual(['SUCCEEDED']);
});
it('restores the previous gateway processes when the new process set cannot start', async () => {
const workspace = await createReleaseWorkspace();
const harness = createRepository();