Merge remote-tracking branch 'origin/main' into feature/ui-action-feedback-20260811
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
export interface BuildCommand {
|
export interface BuildCommand {
|
||||||
command: string;
|
command: string;
|
||||||
@@ -25,6 +26,35 @@ export interface BuildRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MAX_BUILD_OUTPUT_CHARS = 64 * 1024;
|
export const MAX_BUILD_OUTPUT_CHARS = 64 * 1024;
|
||||||
|
export const RELEASE_TURBO_CONCURRENCY = 2;
|
||||||
|
|
||||||
|
export const resolveReleaseTurboCacheDir = (cacheAnchorRoot: string, env?: Record<string, string>): string => {
|
||||||
|
const configured = env?.TURBO_CACHE_DIR?.trim();
|
||||||
|
if (!configured) return path.join(path.resolve(cacheAnchorRoot), '.turbo', 'release-cache');
|
||||||
|
return path.isAbsolute(configured) ? configured : path.resolve(cacheAnchorRoot, configured);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildTurboReleaseCommand = (
|
||||||
|
workspaceRoot: string,
|
||||||
|
cacheAnchorRoot: string,
|
||||||
|
packageNames: string[],
|
||||||
|
env?: Record<string, string>
|
||||||
|
): BuildCommand => ({
|
||||||
|
command: 'pnpm',
|
||||||
|
args: [
|
||||||
|
'exec',
|
||||||
|
'turbo',
|
||||||
|
'run',
|
||||||
|
'build',
|
||||||
|
...packageNames.map((packageName) => `--filter=${packageName}`),
|
||||||
|
`--cache-dir=${resolveReleaseTurboCacheDir(cacheAnchorRoot, env)}`,
|
||||||
|
`--concurrency=${RELEASE_TURBO_CONCURRENCY}`,
|
||||||
|
'--ui=stream',
|
||||||
|
'--output-logs=new-only',
|
||||||
|
],
|
||||||
|
cwd: workspaceRoot,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
const appendOutputTail = (current: string, chunk: unknown): string =>
|
const appendOutputTail = (current: string, chunk: unknown): string =>
|
||||||
`${current}${String(chunk)}`.slice(-MAX_BUILD_OUTPUT_CHARS);
|
`${current}${String(chunk)}`.slice(-MAX_BUILD_OUTPUT_CHARS);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@sammo-ts/infra';
|
} from '@sammo-ts/infra';
|
||||||
import { isRecord } from '@sammo-ts/common';
|
import { isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
import type { BuildCommand, BuildRunner } from './buildRunner.js';
|
import { buildTurboReleaseCommand, type BuildCommand, type BuildRunner } from './buildRunner.js';
|
||||||
import { sanitizeManagedProcessEnv, type ProcessManager } from './processManager.js';
|
import { sanitizeManagedProcessEnv, type ProcessManager } from './processManager.js';
|
||||||
import type {
|
import type {
|
||||||
GatewayClaimedProfileUpdate,
|
GatewayClaimedProfileUpdate,
|
||||||
@@ -494,7 +494,8 @@ export const buildProfileFrontendCommands = (
|
|||||||
export const buildWorkspaceCommands = (
|
export const buildWorkspaceCommands = (
|
||||||
workspaceRoot: string,
|
workspaceRoot: string,
|
||||||
needsInstall: boolean,
|
needsInstall: boolean,
|
||||||
env?: Record<string, string>
|
env?: Record<string, string>,
|
||||||
|
cacheAnchorRoot: string = workspaceRoot
|
||||||
): BuildCommand[] => {
|
): BuildCommand[] => {
|
||||||
const commands: BuildCommand[] = [];
|
const commands: BuildCommand[] = [];
|
||||||
if (needsInstall) {
|
if (needsInstall) {
|
||||||
@@ -505,23 +506,9 @@ export const buildWorkspaceCommands = (
|
|||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const buildSteps: Array<[filter: string, script: string]> = [
|
commands.push(
|
||||||
['@sammo-ts/common', 'build'],
|
buildTurboReleaseCommand(workspaceRoot, cacheAnchorRoot, ['@sammo-ts/game-api', '@sammo-ts/gateway-api'], env)
|
||||||
['@sammo-ts/infra', 'prisma:generate'],
|
);
|
||||||
['@sammo-ts/infra', 'build'],
|
|
||||||
['@sammo-ts/logic', 'build'],
|
|
||||||
['@sammo-ts/game-api', 'build'],
|
|
||||||
['@sammo-ts/game-engine', 'build'],
|
|
||||||
['@sammo-ts/gateway-api', 'build'],
|
|
||||||
];
|
|
||||||
for (const [filter, script] of buildSteps) {
|
|
||||||
commands.push({
|
|
||||||
command: 'pnpm',
|
|
||||||
args: ['--filter', filter, script],
|
|
||||||
cwd: workspaceRoot,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return commands;
|
return commands;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1061,7 +1048,12 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
|||||||
const manifest = await readReleaseManifest(workspace.root);
|
const manifest = await readReleaseManifest(workspace.root);
|
||||||
assertReleaseComponents(manifest, ['game-api', 'game-engine', 'game-frontend']);
|
assertReleaseComponents(manifest, ['game-api', 'game-engine', 'game-frontend']);
|
||||||
const commands = [
|
const commands = [
|
||||||
...buildWorkspaceCommands(workspace.root, workspace.needsInstall, this.processConfig.baseEnv),
|
...buildWorkspaceCommands(
|
||||||
|
workspace.root,
|
||||||
|
workspace.needsInstall,
|
||||||
|
this.processConfig.baseEnv,
|
||||||
|
this.processConfig.workspaceRoot
|
||||||
|
),
|
||||||
...buildProfileFrontendCommands(workspace.root, profile, this.processConfig.baseEnv),
|
...buildProfileFrontendCommands(workspace.root, profile, this.processConfig.baseEnv),
|
||||||
];
|
];
|
||||||
const result = await this.buildRunner.run(commands);
|
const result = await this.buildRunner.run(commands);
|
||||||
@@ -1552,7 +1544,12 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
|||||||
}> {
|
}> {
|
||||||
const workspace = await this.workspaceManager.prepare(commitSha);
|
const workspace = await this.workspaceManager.prepare(commitSha);
|
||||||
const commands = [
|
const commands = [
|
||||||
...buildWorkspaceCommands(workspace.root, workspace.needsInstall, this.processConfig.baseEnv),
|
...buildWorkspaceCommands(
|
||||||
|
workspace.root,
|
||||||
|
workspace.needsInstall,
|
||||||
|
this.processConfig.baseEnv,
|
||||||
|
this.processConfig.workspaceRoot
|
||||||
|
),
|
||||||
...(profile ? buildProfileFrontendCommands(workspace.root, profile, this.processConfig.baseEnv) : []),
|
...(profile ? buildProfileFrontendCommands(workspace.root, profile, this.processConfig.baseEnv) : []),
|
||||||
];
|
];
|
||||||
return { result: await this.buildRunner.run(commands), workspace };
|
return { result: await this.buildRunner.run(commands), workspace };
|
||||||
|
|||||||
@@ -2,7 +2,54 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { MAX_BUILD_OUTPUT_CHARS, PnpmBuildRunner } from '../src/orchestrator/buildRunner.js';
|
import {
|
||||||
|
buildTurboReleaseCommand,
|
||||||
|
MAX_BUILD_OUTPUT_CHARS,
|
||||||
|
PnpmBuildRunner,
|
||||||
|
resolveReleaseTurboCacheDir,
|
||||||
|
} from '../src/orchestrator/buildRunner.js';
|
||||||
|
|
||||||
|
describe('Turbo release build plan', () => {
|
||||||
|
it('anchors the default cache outside commit worktrees and allows an operator override', () => {
|
||||||
|
expect(resolveReleaseTurboCacheDir('/srv/core/repository')).toBe('/srv/core/repository/.turbo/release-cache');
|
||||||
|
expect(
|
||||||
|
resolveReleaseTurboCacheDir('/srv/core/repository', {
|
||||||
|
TURBO_CACHE_DIR: '/srv/core/cache/turbo',
|
||||||
|
})
|
||||||
|
).toBe('/srv/core/cache/turbo');
|
||||||
|
expect(
|
||||||
|
resolveReleaseTurboCacheDir('/srv/core/repository', {
|
||||||
|
TURBO_CACHE_DIR: '.cache/turbo',
|
||||||
|
})
|
||||||
|
).toBe('/srv/core/repository/.cache/turbo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses a bounded streaming Turbo build for the selected packages', () => {
|
||||||
|
expect(
|
||||||
|
buildTurboReleaseCommand(
|
||||||
|
'/srv/core/profile-worktrees/commit',
|
||||||
|
'/srv/core/repository',
|
||||||
|
['@sammo-ts/game-api'],
|
||||||
|
{ NODE_ENV: 'production' }
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
command: 'pnpm',
|
||||||
|
args: [
|
||||||
|
'exec',
|
||||||
|
'turbo',
|
||||||
|
'run',
|
||||||
|
'build',
|
||||||
|
'--filter=@sammo-ts/game-api',
|
||||||
|
'--cache-dir=/srv/core/repository/.turbo/release-cache',
|
||||||
|
'--concurrency=2',
|
||||||
|
'--ui=stream',
|
||||||
|
'--output-logs=new-only',
|
||||||
|
],
|
||||||
|
cwd: '/srv/core/profile-worktrees/commit',
|
||||||
|
env: { NODE_ENV: 'production' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('PnpmBuildRunner', () => {
|
describe('PnpmBuildRunner', () => {
|
||||||
it('returns a failed result when a command cannot be spawned', async () => {
|
it('returns a failed result when a command cannot be spawned', async () => {
|
||||||
|
|||||||
@@ -225,17 +225,22 @@ describe('sanitizeManagedProcessEnv', () => {
|
|||||||
describe('buildWorkspaceCommands', () => {
|
describe('buildWorkspaceCommands', () => {
|
||||||
it('installs and builds runtime dependencies before the profile processes', () => {
|
it('installs and builds runtime dependencies before the profile processes', () => {
|
||||||
const workspaceRoot = '/srv/sammo/worktrees/0123456789abcdef';
|
const workspaceRoot = '/srv/sammo/worktrees/0123456789abcdef';
|
||||||
const commands = buildWorkspaceCommands(workspaceRoot, true);
|
const commands = buildWorkspaceCommands(workspaceRoot, true, undefined, '/srv/sammo/controller');
|
||||||
|
|
||||||
expect(commands.map(({ args }) => args)).toEqual([
|
expect(commands.map(({ args }) => args)).toEqual([
|
||||||
['install', '--frozen-lockfile'],
|
['install', '--frozen-lockfile'],
|
||||||
['--filter', '@sammo-ts/common', 'build'],
|
[
|
||||||
['--filter', '@sammo-ts/infra', 'prisma:generate'],
|
'exec',
|
||||||
['--filter', '@sammo-ts/infra', 'build'],
|
'turbo',
|
||||||
['--filter', '@sammo-ts/logic', 'build'],
|
'run',
|
||||||
['--filter', '@sammo-ts/game-api', 'build'],
|
'build',
|
||||||
['--filter', '@sammo-ts/game-engine', 'build'],
|
'--filter=@sammo-ts/game-api',
|
||||||
['--filter', '@sammo-ts/gateway-api', 'build'],
|
'--filter=@sammo-ts/gateway-api',
|
||||||
|
'--cache-dir=/srv/sammo/controller/.turbo/release-cache',
|
||||||
|
'--concurrency=2',
|
||||||
|
'--ui=stream',
|
||||||
|
'--output-logs=new-only',
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
expect(commands.every(({ cwd }) => cwd === workspaceRoot)).toBe(true);
|
expect(commands.every(({ cwd }) => cwd === workspaceRoot)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ Gateway process 환경에 전달하지 않습니다. 이 값이 frontend 정의
|
|||||||
frontend build 계약입니다.
|
frontend build 계약입니다.
|
||||||
- `RELEASE_CONTROLLER_POLL_MS`, `RELEASE_CONTROLLER_READINESS_TIMEOUT_MS`: queue
|
- `RELEASE_CONTROLLER_POLL_MS`, `RELEASE_CONTROLLER_READINESS_TIMEOUT_MS`: queue
|
||||||
poll과 준비 제한 시간입니다.
|
poll과 준비 제한 시간입니다.
|
||||||
|
- `TURBO_CACHE_DIR`: 선택 사항인 공유 local cache 경로입니다. 없으면 원래
|
||||||
|
`RELEASE_CONTROLLER_WORKSPACE_ROOT/.turbo/release-cache`를 사용합니다. 상대 경로는
|
||||||
|
원래 workspace 기준으로 해석합니다.
|
||||||
|
|
||||||
비밀값은 Git에서 제외된 환경 파일 또는 process 환경으로 전달해 주세요.
|
비밀값은 Git에서 제외된 환경 파일 또는 process 환경으로 전달해 주세요.
|
||||||
`VITE_*`에는 공개 URL만 넣어 주세요.
|
`VITE_*`에는 공개 URL만 넣어 주세요.
|
||||||
@@ -46,13 +49,7 @@ DEPLOY의 rollback이 frontend build가 없는 controller worktree를 이전 Gat
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
pnpm --filter @sammo-ts/infra prisma:generate
|
pnpm exec turbo run build --filter=@sammo-ts/release-controller --concurrency=2 --ui=stream
|
||||||
pnpm --filter @sammo-ts/common build
|
|
||||||
pnpm --filter @sammo-ts/infra build
|
|
||||||
pnpm --filter @sammo-ts/logic build
|
|
||||||
pnpm --filter @sammo-ts/game-engine build
|
|
||||||
pnpm --filter @sammo-ts/gateway-api build
|
|
||||||
pnpm --filter @sammo-ts/release-controller build
|
|
||||||
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:gateway
|
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:gateway
|
||||||
pnpm --filter @sammo-ts/release-controller start
|
pnpm --filter @sammo-ts/release-controller start
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { stripVTControlCharacters } from 'node:util';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
assertReleaseComponents,
|
assertReleaseComponents,
|
||||||
|
buildTurboReleaseCommand,
|
||||||
type BuildCommand,
|
type BuildCommand,
|
||||||
type BuildProgressEvent,
|
type BuildProgressEvent,
|
||||||
type BuildRunner,
|
type BuildRunner,
|
||||||
@@ -38,13 +39,12 @@ export const buildGatewayReleaseCommands = (
|
|||||||
};
|
};
|
||||||
return [
|
return [
|
||||||
...(needsInstall ? [{ command: 'pnpm', args: ['install', '--frozen-lockfile'], cwd: workspaceRoot, env }] : []),
|
...(needsInstall ? [{ command: 'pnpm', args: ['install', '--frozen-lockfile'], cwd: workspaceRoot, env }] : []),
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/common', 'build'], cwd: workspaceRoot, env },
|
buildTurboReleaseCommand(
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/infra', 'prisma:generate'], cwd: workspaceRoot, env },
|
workspaceRoot,
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/infra', 'build'], cwd: workspaceRoot, env },
|
config.workspaceRoot,
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/logic', 'build'], cwd: workspaceRoot, env },
|
['@sammo-ts/gateway-api', '@sammo-ts/gateway-frontend'],
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/game-engine', 'build'], cwd: workspaceRoot, env },
|
env
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/gateway-api', 'build'], cwd: workspaceRoot, env },
|
),
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/gateway-frontend', 'build'], cwd: workspaceRoot, env },
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -244,7 +244,12 @@ export class GatewayReleaseController {
|
|||||||
await this.startDefinitions(buildGatewayProcessDefinitions(workspace.root, this.config), operation.id);
|
await this.startDefinitions(buildGatewayProcessDefinitions(workspace.root, this.config), operation.id);
|
||||||
await this.waitForReadiness(operation.id);
|
await this.waitForReadiness(operation.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.appendLog(operation.id, 'rollback', '새 Gateway 시작에 실패하여 이전 process를 복구합니다.', 'ERROR');
|
await this.appendLog(
|
||||||
|
operation.id,
|
||||||
|
'rollback',
|
||||||
|
'새 Gateway 시작에 실패하여 이전 process를 복구합니다.',
|
||||||
|
'ERROR'
|
||||||
|
);
|
||||||
await this.stopManagedProcesses(operation.id);
|
await this.stopManagedProcesses(operation.id);
|
||||||
if (previousDefinitions.length) {
|
if (previousDefinitions.length) {
|
||||||
await this.startDefinitions(previousDefinitions, operation.id);
|
await this.startDefinitions(previousDefinitions, operation.id);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
assertReleaseComponents,
|
assertReleaseComponents,
|
||||||
|
buildTurboReleaseCommand,
|
||||||
type BuildCommand,
|
type BuildCommand,
|
||||||
type BuildRunner,
|
type BuildRunner,
|
||||||
type GitWorkspaceManager,
|
type GitWorkspaceManager,
|
||||||
@@ -24,13 +25,7 @@ export const buildReleaseControllerCommands = (
|
|||||||
const env = sanitizeManagedProcessEnv(config.baseEnv);
|
const env = sanitizeManagedProcessEnv(config.baseEnv);
|
||||||
return [
|
return [
|
||||||
...(needsInstall ? [{ command: 'pnpm', args: ['install', '--frozen-lockfile'], cwd: workspaceRoot, env }] : []),
|
...(needsInstall ? [{ command: 'pnpm', args: ['install', '--frozen-lockfile'], cwd: workspaceRoot, env }] : []),
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/common', 'build'], cwd: workspaceRoot, env },
|
buildTurboReleaseCommand(workspaceRoot, config.workspaceRoot, ['@sammo-ts/release-controller'], env),
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/infra', 'prisma:generate'], cwd: workspaceRoot, env },
|
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/infra', 'build'], cwd: workspaceRoot, env },
|
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/logic', 'build'], cwd: workspaceRoot, env },
|
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/game-engine', 'build'], cwd: workspaceRoot, env },
|
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/gateway-api', 'build'], cwd: workspaceRoot, env },
|
|
||||||
{ command: 'pnpm', args: ['--filter', '@sammo-ts/release-controller', 'build'], cwd: workspaceRoot, env },
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ describe('GatewayReleaseController', () => {
|
|||||||
|
|
||||||
expect(commandGroups).toHaveLength(2);
|
expect(commandGroups).toHaveLength(2);
|
||||||
expect(commandGroups[0]?.[0]).toBe('install --frozen-lockfile');
|
expect(commandGroups[0]?.[0]).toBe('install --frozen-lockfile');
|
||||||
|
expect(commandGroups[0]?.[1]).toContain('turbo run build');
|
||||||
|
expect(commandGroups[0]?.[1]).toContain('--cache-dir=/srv/sammo/controller/.turbo/release-cache');
|
||||||
expect(commandGroups[1]).toEqual(['--filter @sammo-ts/infra prisma:migrate:deploy:gateway']);
|
expect(commandGroups[1]).toEqual(['--filter @sammo-ts/infra prisma:migrate:deploy:gateway']);
|
||||||
expect([...running.keys()].sort()).toEqual([...gatewayNames].sort());
|
expect([...running.keys()].sort()).toEqual([...gatewayNames].sort());
|
||||||
expect(harness.published).toEqual([
|
expect(harness.published).toEqual([
|
||||||
@@ -216,7 +218,16 @@ describe('GatewayReleaseController', () => {
|
|||||||
]);
|
]);
|
||||||
expect(harness.completions).toEqual(['SUCCEEDED']);
|
expect(harness.completions).toEqual(['SUCCEEDED']);
|
||||||
expect(harness.logs.map((entry) => entry.phase)).toEqual(
|
expect(harness.logs.map((entry) => entry.phase)).toEqual(
|
||||||
expect.arrayContaining(['claim', 'resolve', 'workspace', 'build', 'migration', 'switch', 'readiness', 'publish'])
|
expect.arrayContaining([
|
||||||
|
'claim',
|
||||||
|
'resolve',
|
||||||
|
'workspace',
|
||||||
|
'build',
|
||||||
|
'migration',
|
||||||
|
'switch',
|
||||||
|
'readiness',
|
||||||
|
'publish',
|
||||||
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -275,8 +286,7 @@ describe('GatewayReleaseController', () => {
|
|||||||
await onProgress?.({
|
await onProgress?.({
|
||||||
type: 'OUTPUT',
|
type: 'OUTPUT',
|
||||||
stream: 'stdout',
|
stream: 'stdout',
|
||||||
message:
|
message: 'bootstrap-secret-value postgresql://operator:visible-password@db.invalid/sammo',
|
||||||
'bootstrap-secret-value postgresql://operator:visible-password@db.invalid/sammo',
|
|
||||||
});
|
});
|
||||||
return { ok: true, exitCode: 0, output: '' };
|
return { ok: true, exitCode: 0, output: '' };
|
||||||
},
|
},
|
||||||
@@ -405,7 +415,7 @@ describe('upgradeReleaseController', () => {
|
|||||||
).resolves.toEqual({ commitSha: SHA, workspace });
|
).resolves.toEqual({ commitSha: SHA, workspace });
|
||||||
|
|
||||||
expect(commandGroups).toHaveLength(2);
|
expect(commandGroups).toHaveLength(2);
|
||||||
expect(commandGroups[0]?.at(-1)).toBe('--filter @sammo-ts/release-controller build');
|
expect(commandGroups[0]?.at(-1)).toContain('turbo run build --filter=@sammo-ts/release-controller');
|
||||||
expect(starts.at(-1)).toMatchObject({
|
expect(starts.at(-1)).toMatchObject({
|
||||||
name: 'sammo:release-controller',
|
name: 'sammo:release-controller',
|
||||||
cwd: path.join(workspace, 'app', 'release-controller'),
|
cwd: path.join(workspace, 'app', 'release-controller'),
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ profile 범위 권한과 별개인 전역 `admin.releases.manage` 권한이 필
|
|||||||
- Root와 server package의 `tsdown`은 0.22.14 계열로 통일합니다. Docker runtime의
|
- Root와 server package의 `tsdown`은 0.22.14 계열로 통일합니다. Docker runtime의
|
||||||
Node heap/Rayon 상한을 상속한 동일 toolchain으로 초기 Gateway와 profile
|
Node heap/Rayon 상한을 상속한 동일 toolchain으로 초기 Gateway와 profile
|
||||||
worktree를 빌드하여 구형 Rolldown의 과도한 native thread 생성을 피합니다.
|
worktree를 빌드하여 구형 Rolldown의 과도한 native thread 생성을 피합니다.
|
||||||
|
- Profile, Gateway와 controller self-upgrade의 server package build는 Turbo DAG를
|
||||||
|
동시성 2로 실행합니다. 기본 local cache는 원래 Core checkout의
|
||||||
|
`.turbo/release-cache`이므로 commit별 worktree가 달라도 재사용됩니다. 별도
|
||||||
|
persistent 경로가 필요하면 controller/orchestrator 환경에 `TURBO_CACHE_DIR`을
|
||||||
|
설정합니다. Cache는 재생성 가능한 build artifact이며 DB/Redis backup이 아닙니다.
|
||||||
|
- `NODE_ENV`와 Vite가 추론한 `VITE_*`는 build hash에 포함됩니다. 따라서 base path나
|
||||||
|
API URL이 다른 frontend artifact를 cache hit로 잘못 복원하지 않습니다.
|
||||||
|
`NODE_OPTIONS`와 `RAYON_NUM_THREADS`는 출력에는 영향을 주지 않는 resource 제한으로
|
||||||
|
build child에 전달됩니다.
|
||||||
- migration 이후 이전 애플리케이션으로 돌아갈 때 schema 하위 호환성이
|
- migration 이후 이전 애플리케이션으로 돌아갈 때 schema 하위 호환성이
|
||||||
유지됩니다.
|
유지됩니다.
|
||||||
|
|
||||||
@@ -113,7 +122,7 @@ Gateway는 자기 process를 직접 교체하지 않습니다. 관리자 화면
|
|||||||
|
|
||||||
1. Source ref를 commit SHA로 고정하고 commit worktree를 준비합니다.
|
1. Source ref를 commit SHA로 고정하고 commit worktree를 준비합니다.
|
||||||
2. Release manifest의 protocol, component와 migration head를 검증합니다.
|
2. Release manifest의 protocol, component와 migration head를 검증합니다.
|
||||||
3. Gateway API와 frontend를 빌드하고 gateway migration을 적용합니다.
|
3. Gateway API와 frontend를 공유 Turbo cache로 빌드하고 gateway migration을 적용합니다.
|
||||||
4. `sammo:gateway-api`, `sammo:gateway-frontend`,
|
4. `sammo:gateway-api`, `sammo:gateway-frontend`,
|
||||||
`sammo:gateway-orchestrator`를 새 worktree definition으로 전환합니다.
|
`sammo:gateway-orchestrator`를 새 worktree definition으로 전환합니다.
|
||||||
5. Gateway API `/healthz`, `/gateway/`와 세 PM2 process의 `online` 상태를
|
5. Gateway API `/healthz`, `/gateway/`와 세 PM2 process의 `online` 상태를
|
||||||
@@ -137,6 +146,8 @@ Gateway 전체에는 활성 릴리스 작업을 동시에 하나만 둘 수 있
|
|||||||
commit 해석, worktree 준비, build 명령 출력, migration, process 전환,
|
commit 해석, worktree 준비, build 명령 출력, migration, process 전환,
|
||||||
readiness와 rollback 진행을 커서 순서대로 이어 붙입니다. 완료된 작업의 로그도
|
readiness와 rollback 진행을 커서 순서대로 이어 붙입니다. 완료된 작업의 로그도
|
||||||
같은 이력에서 다시 열 수 있으며 화면은 최근 1,000줄을 유지합니다.
|
같은 이력에서 다시 열 수 있으며 화면은 최근 1,000줄을 유지합니다.
|
||||||
|
Build 로그의 `cache hit`/`cache miss`와 마지막 `Cached: N cached, M total`은 실제
|
||||||
|
이번 릴리스의 cache 사용 여부를 나타냅니다.
|
||||||
|
|
||||||
로그 원본은 Gateway DB의 `GatewayReleaseLog`에 작업별로 저장되고 작업 삭제 시
|
로그 원본은 Gateway DB의 `GatewayReleaseLog`에 작업별로 저장되고 작업 삭제 시
|
||||||
함께 제거됩니다. Controller는 ANSI 제어 문자를 제거하고 secret·token·password
|
함께 제거됩니다. Controller는 ANSI 제어 문자를 제거하고 secret·token·password
|
||||||
|
|||||||
+5
-2
@@ -1,17 +1,20 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
"ui": "tui",
|
"ui": "tui",
|
||||||
|
"globalPassThroughEnv": ["NODE_OPTIONS", "RAYON_NUM_THREADS"],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build", "typecheck"],
|
"dependsOn": ["^build", "typecheck"],
|
||||||
|
"env": ["NODE_ENV"],
|
||||||
|
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||||
"outputs": ["dist/**"]
|
"outputs": ["dist/**"]
|
||||||
},
|
},
|
||||||
"typecheck": {
|
"typecheck": {
|
||||||
"dependsOn": ["^typecheck", "prisma:generate"],
|
"dependsOn": ["^build", "prisma:generate"],
|
||||||
"cache": true
|
"cache": true
|
||||||
},
|
},
|
||||||
"prisma:generate": {
|
"prisma:generate": {
|
||||||
"inputs": ["prisma/*.prisma"],
|
"inputs": ["$TURBO_DEFAULT$"],
|
||||||
"outputs": ["node_modules/.prisma/client/**", "prisma/generated/**"],
|
"outputs": ["node_modules/.prisma/client/**", "prisma/generated/**"],
|
||||||
"cache": true
|
"cache": true
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user