feat: 오래된 프런트엔드 빌드 자산을 안전하게 정리한다

현재·이전 release와 공유 asset 의존성, 진행 중 commit을 보호하고 24시간 유예 및 최신 2개 cache를 적용한다.

Gateway와 profile daemon의 기존 24시간 관리 주기에 artifact cleanup을 포함하고 fail-closed 회귀 테스트와 운영 문서를 보강한다.
This commit is contained in:
2026-08-23 01:55:22 +00:00
parent ade543f936
commit 0e3292f591
9 changed files with 715 additions and 34 deletions
@@ -6,6 +6,8 @@ import {
assertReleaseComponents,
buildTurboReleaseCommand,
buildTurboReleaseTaskCommand,
DEFAULT_FRONTEND_ARTIFACT_KEEP_NEWEST,
DEFAULT_FRONTEND_ARTIFACT_RETENTION_MS,
DEFAULT_MANAGED_WORKSPACE_KEEP_NEWEST,
DEFAULT_MANAGED_WORKSPACE_RETENTION_MS,
type BuildCommand,
@@ -14,6 +16,7 @@ import {
type GatewayReleaseOperationRecord,
type GatewayReleaseRepository,
type GatewayReleaseStateRecord,
type FrontendArtifactCleanupResult,
type GitWorkspaceManager,
type ProcessDefinition,
type ProcessManager,
@@ -34,6 +37,11 @@ const MANAGED_PROCESS_NAMES = ['sammo:gateway-api', 'sammo:gateway-frontend', 's
const SENSITIVE_ENV_NAME = /(SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|CLIENT_SECRET|DATABASE_URL|REDIS_URL)/iu;
export const RELEASE_WORKSPACE_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1_000;
export interface ReleaseManagedCleanupResult {
workspaces: { removed: string[]; skipped: string[] };
artifacts: FrontendArtifactCleanupResult;
}
const isRuntimeProcessActive = (status: string): boolean =>
['online', 'launching', 'stopping'].includes(status.toLowerCase());
@@ -183,6 +191,36 @@ export class GatewayReleaseController {
});
}
async cleanupStaleResources(): Promise<ReleaseManagedCleanupResult> {
const workspaces = await this.cleanupStaleWorkspaces();
let artifacts: FrontendArtifactCleanupResult = { removed: [], retained: [], skipped: [] };
if (this.config.frontendServeMode === 'static') {
const [state, operations] = await Promise.all([
this.repository.getState(),
this.repository.listOperations(100),
]);
artifacts = await this.artifactManager.cleanup({
frontendKeys: ['gateway'],
protectedCommitShas: [
...[state.activeCommitSha, state.previousCommitSha].filter((commitSha): commitSha is string =>
Boolean(commitSha)
),
...operations
.filter(
(operation) =>
operation.resolvedCommitSha &&
(operation.status === 'QUEUED' || operation.status === 'RUNNING')
)
.map((operation) => operation.resolvedCommitSha as string),
],
retentionMs: DEFAULT_FRONTEND_ARTIFACT_RETENTION_MS,
keepNewest: DEFAULT_FRONTEND_ARTIFACT_KEEP_NEWEST,
now: this.now(),
});
}
return { workspaces, artifacts };
}
private sanitizeLogMessage(message: string): string {
let sanitized = stripVTControlCharacters(message);
const sensitiveValues = new Set([