perf: 릴리스 프론트엔드 빌드를 안전하게 캐시

Gateway와 Profile 릴리스의 타입검사와 Vite 번들을 별도 Turbo 작업으로 캐시한다. DB 보존 DEPLOY는 game-api만 빌드하고 기본 동시성 1과 기존 산출물 계약은 유지한다.
This commit is contained in:
2026-08-19 18:05:38 +00:00
parent 9390195d5f
commit b9d5edbb3d
14 changed files with 261 additions and 24 deletions
+23
View File
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest';
import {
buildTurboReleaseCommand,
buildTurboReleaseTaskCommand,
MAX_BUILD_OUTPUT_CHARS,
PnpmBuildRunner,
resolveReleaseTurboCacheDir,
@@ -58,6 +59,28 @@ describe('Turbo release build plan', () => {
env: { NODE_ENV: 'production' },
});
});
it('uses the same bounded cache policy for a release-specific task', () => {
expect(
buildTurboReleaseTaskCommand(
'/srv/core/profile-worktrees/commit',
'/srv/core/repository',
'build:release',
['@sammo-ts/game-frontend'],
{ VITE_APP_BASE_PATH: '/che' }
).args
).toEqual([
'exec',
'turbo',
'run',
'build:release',
'--filter=@sammo-ts/game-frontend',
'--cache-dir=/srv/core/repository/.turbo/release-cache',
'--concurrency=1',
'--ui=stream',
'--output-logs=new-only',
]);
});
});
describe('PnpmBuildRunner', () => {
@@ -315,6 +315,20 @@ describe('buildWorkspaceCommands', () => {
expect(commands.every(({ cwd }) => cwd === workspaceRoot)).toBe(true);
});
it('can limit a DB-preserving deploy to the game runtime server target', () => {
const commands = buildWorkspaceCommands(
'/srv/sammo/worktrees/0123456789abcdef',
false,
undefined,
'/srv/sammo/controller',
['@sammo-ts/game-api']
);
expect(commands[0]?.args).toContain('--filter=@sammo-ts/game-api');
expect(commands[0]?.args).not.toContain('--filter=@sammo-ts/gateway-api');
expect(commands[0]?.args).toContain('--concurrency=1');
});
it('deploys the game schema migration after building the selected workspace', () => {
const workspaceRoot = '/srv/sammo/worktrees/0123456789abcdef';
const databaseUrl = 'postgresql://integration.invalid/sammo?schema=che';
@@ -347,6 +361,18 @@ describe('buildProfileFrontendCommands', () => {
(command) => command.env?.PROFILE_FRONTEND_BUILD_NODE_OPTIONS === '--max-old-space-size=2048'
)
).toBe(true);
expect(commands[0]?.args).toEqual([
'exec',
'turbo',
'run',
'build:release',
'--filter=@sammo-ts/game-frontend',
'--cache-dir=/srv/sammo/worktrees/0123456789abcdef/.turbo/release-cache',
'--concurrency=1',
'--ui=stream',
'--output-logs=new-only',
]);
expect(commands[1]?.args).toEqual(['tools/build-scripts/materialize-profile-frontend.mjs', 'che:2']);
});
it('keeps the shared Node heap when no frontend build override is configured', () => {
@@ -185,6 +185,15 @@ describe('profile DEPLOY operation', () => {
expect(commandGroups).toHaveLength(2);
expect(commandGroups[0]?.[0]?.args).toEqual(['install', '--frozen-lockfile']);
expect(commandGroups[0]?.[1]?.args).toContain('--filter=@sammo-ts/game-api');
expect(commandGroups[0]?.[1]?.args).not.toContain('--filter=@sammo-ts/gateway-api');
expect(commandGroups[0]?.[2]?.args).toContain('build:release');
expect(commandGroups[0]?.[2]?.args).toContain('--cache-dir=/srv/sammo/controller/.turbo/release-cache');
expect(commandGroups[0]?.[2]?.args).toContain('--concurrency=1');
expect(commandGroups[0]?.[3]?.args).toEqual([
'tools/build-scripts/materialize-profile-frontend.mjs',
'che:1010',
]);
expect(commandGroups[1]?.map((command) => command.args)).toEqual([
['--filter', '@sammo-ts/infra', 'prisma:migrate:deploy:game'],
]);
@@ -0,0 +1,45 @@
import { execFile } from 'node:child_process';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { promisify } from 'node:util';
import { afterEach, describe, expect, it } from 'vitest';
const execFileAsync = promisify(execFile);
const materializer = path.resolve(import.meta.dirname, '../../../tools/build-scripts/materialize-profile-frontend.mjs');
const temporaryRoots: string[] = [];
afterEach(async () => {
await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
describe('profile frontend materializer', () => {
it('replaces an existing profile artifact from the cached release build', async () => {
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), 'profile-frontend-materializer-'));
temporaryRoots.push(workspaceRoot);
const source = path.join(workspaceRoot, 'app', 'game-frontend', '.release-build');
const target = path.join(workspaceRoot, '.release-dist', 'che_2', 'game-frontend');
await mkdir(source, { recursive: true });
await mkdir(target, { recursive: true });
await writeFile(path.join(source, 'index.html'), 'new release');
await writeFile(path.join(target, 'index.html'), 'old release');
await execFileAsync(process.execPath, [materializer, 'che:2'], { cwd: workspaceRoot });
await expect(readFile(path.join(target, 'index.html'), 'utf8')).resolves.toBe('new release');
});
it('keeps an existing artifact untouched when the cached build is missing', async () => {
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), 'profile-frontend-materializer-'));
temporaryRoots.push(workspaceRoot);
const target = path.join(workspaceRoot, '.release-dist', 'che_2', 'game-frontend');
await mkdir(target, { recursive: true });
await writeFile(path.join(target, 'index.html'), 'old release');
await expect(
execFileAsync(process.execPath, [materializer, 'che:2'], { cwd: workspaceRoot })
).rejects.toThrow();
await expect(readFile(path.join(target, 'index.html'), 'utf8')).resolves.toBe('old release');
});
});