feat(release): 정적 프런트엔드와 격리 빌더를 도입한다

Gateway와 프로필 릴리스가 불변 프런트엔드 아티팩트를 원자적으로 게시하고 실패 시 이전 포인터를 복구하도록 한다.

릴리스 빌드는 전용 builder protocol로 분리하고 성공한 Gateway 커밋을 재기동용 Git ref에 고정한다.
This commit is contained in:
2026-08-22 09:32:47 +00:00
parent d74a8e48bb
commit d3e5d12cbf
18 changed files with 991 additions and 48 deletions
@@ -37,6 +37,17 @@ const createReleaseWorkspace = async (): Promise<string> => {
components: ['game-api', 'game-engine', 'game-frontend'],
})
);
await fs.mkdir(path.join(workspace, '.release-dist', 'che_1010', 'game-frontend', 'assets'), {
recursive: true,
});
await fs.writeFile(
path.join(workspace, '.release-dist', 'che_1010', 'game-frontend', 'index.html'),
'<!doctype html><title>static profile</title>'
);
await fs.writeFile(
path.join(workspace, '.release-dist', 'che_1010', 'game-frontend', 'assets', 'app-deadbeef.js'),
'console.log("static")'
);
return workspace;
};
@@ -45,7 +56,7 @@ afterEach(async () => {
});
describe('profile DEPLOY operation', () => {
it('migrates and switches the selected release without executing the reset seed path', async () => {
it('migrates and atomically switches a static frontend without executing the reset seed path', async () => {
const workspace = await createReleaseWorkspace();
const profile: GatewayProfileRecord = {
profileName: 'che:1010',
@@ -131,6 +142,7 @@ describe('profile DEPLOY operation', () => {
'sammo:che:1010:battle-sim-worker',
'sammo:che:1010:tournament-worker',
];
const backendProcessNames = processNames.filter((name) => !name.endsWith(':game-frontend'));
const running = new Set(processNames);
const processManager: ProcessManager = {
list: async () => [...running].map((name) => ({ name, status: 'online' })),
@@ -171,6 +183,9 @@ describe('profile DEPLOY operation', () => {
redisKeyPrefix: 'sammo:test',
gameTokenSecret: 'test-secret',
gatewayInternalApiUrl: 'http://127.0.0.1:15001',
frontendServeMode: 'static',
frontendArtifactRoot: path.join(workspace, 'artifact-volume'),
frontendReadinessOrigin: 'http://caddy',
baseEnv: { DATABASE_URL: 'postgresql://user:pass@integration.invalid/sammo' },
},
reconcileIntervalMs: 60_000,
@@ -218,6 +233,9 @@ describe('profile DEPLOY operation', () => {
);
expect(logs.map((entry) => entry.message).join('\n')).not.toContain('pass@integration.invalid');
expect(logs.map((entry) => entry.message).join('\n')).toContain('[REDACTED]');
expect([...running].sort()).toEqual([...processNames].sort());
expect([...running].sort()).toEqual([...backendProcessNames].sort());
expect(
await fs.readFile(path.join(workspace, 'artifact-volume', 'che', 'current', 'index.html'), 'utf8')
).toContain('static profile');
});
});