fix: 실행 중인 릴리스 빌드를 안전하게 중단
Gateway와 프로필 DEPLOY의 빌드 단계만 취소하고 lease와 phase 전환을 직렬화한다. 관리자 화면에 중단·재시도 절차와 회귀 검증을 추가한다.
This commit is contained in:
@@ -145,4 +145,28 @@ describe('PnpmBuildRunner', () => {
|
||||
{ type: 'COMMAND_END' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('terminates a running build process group when the operation is cancelled', async () => {
|
||||
const runner = new PnpmBuildRunner();
|
||||
const abortController = new AbortController();
|
||||
const startedAt = Date.now();
|
||||
const timer = setTimeout(() => abortController.abort(), 50);
|
||||
|
||||
const result = await runner.run(
|
||||
[
|
||||
{
|
||||
command: process.execPath,
|
||||
args: ['-e', "setInterval(() => process.stdout.write('still-running\\n'), 25);"],
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
{ signal: abortController.signal, terminateGraceMs: 100 }
|
||||
);
|
||||
clearTimeout(timer);
|
||||
|
||||
expect(result).toMatchObject({ ok: false, aborted: true });
|
||||
expect(result.output).toContain('Build cancelled by operator.');
|
||||
expect(Date.now() - startedAt).toBeLessThan(2_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -249,6 +249,72 @@ describeDatabase('gateway operation lease and profile serialization', () => {
|
||||
).resolves.toMatchObject({ status: 'SUCCEEDED' });
|
||||
});
|
||||
|
||||
it('cancels only a running profile DEPLOY build and fences its worker lease', async () => {
|
||||
const operation = await repository.createOperation({
|
||||
profileName,
|
||||
type: 'DEPLOY',
|
||||
sourceMode: 'BRANCH',
|
||||
sourceRef: 'main',
|
||||
requestedBy: 'admin',
|
||||
});
|
||||
const now = new Date('2030-01-01T00:00:00.000Z');
|
||||
await repository.claimNextOperation(now, { ownerId: 'worker-a', durationMs: 10_000 });
|
||||
await repository.updateProfileForOperation?.(operation.id, 'worker-a', profileName, {
|
||||
buildStatus: 'RUNNING',
|
||||
buildError: 'temporary build output',
|
||||
});
|
||||
await repository.appendOperationLog(operation.id, {
|
||||
level: 'INFO',
|
||||
phase: 'build',
|
||||
message: 'building profile',
|
||||
});
|
||||
|
||||
await expect(repository.cancelOperation(operation.id)).resolves.toBe(true);
|
||||
await expect(repository.getOperation(operation.id)).resolves.toMatchObject({
|
||||
status: 'CANCELLED',
|
||||
leaseOwner: undefined,
|
||||
});
|
||||
await expect(repository.getProfile(profileName)).resolves.toMatchObject({
|
||||
buildStatus: 'SUCCEEDED',
|
||||
buildError: undefined,
|
||||
});
|
||||
await expect(repository.renewOperationLease?.(operation.id, 'worker-a', now, 10_000)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('cancels a running Gateway build but rejects cancellation after migration starts', async () => {
|
||||
const buildOperation = await releaseRepository.createOperation({
|
||||
type: 'DEPLOY',
|
||||
sourceMode: 'BRANCH',
|
||||
sourceRef: 'main',
|
||||
requestedBy: 'admin',
|
||||
});
|
||||
const now = new Date('2030-01-01T00:00:00.000Z');
|
||||
await releaseRepository.claimNextOperation(now, { ownerId: 'release-worker', durationMs: 10_000 });
|
||||
await releaseRepository.appendOperationLog(buildOperation.id, {
|
||||
level: 'INFO',
|
||||
phase: 'build',
|
||||
message: 'building Gateway',
|
||||
});
|
||||
await expect(releaseRepository.cancelOperation(buildOperation.id)).resolves.toBe(true);
|
||||
await expect(
|
||||
releaseRepository.renewOperationLease(buildOperation.id, 'release-worker', now, 10_000)
|
||||
).resolves.toBe(false);
|
||||
|
||||
const migrationOperation = await releaseRepository.createOperation({
|
||||
type: 'DEPLOY',
|
||||
sourceMode: 'BRANCH',
|
||||
sourceRef: 'main',
|
||||
requestedBy: 'admin',
|
||||
});
|
||||
await releaseRepository.claimNextOperation(now, { ownerId: 'release-worker', durationMs: 10_000 });
|
||||
await releaseRepository.appendOperationLog(migrationOperation.id, {
|
||||
level: 'INFO',
|
||||
phase: 'migration',
|
||||
message: 'migrating Gateway',
|
||||
});
|
||||
await expect(releaseRepository.cancelOperation(migrationOperation.id)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('pins retry to the first resolved commit and preserves its install generation', async () => {
|
||||
const operation = await repository.createOperation({
|
||||
profileName,
|
||||
|
||||
Reference in New Issue
Block a user