merge: 게임 정보 빌드 커밋 표시 통합
This commit is contained in:
@@ -1102,7 +1102,9 @@ test('scopes the new-survey notice cursor to the reset-specific server ID', asyn
|
||||
expect(await page.evaluate(() => localStorage.getItem('state.che.lastVote'))).toBe('99');
|
||||
});
|
||||
|
||||
test('desktop menus preserve ref columns, prefix-safe routes, and controlled dropdown behavior', async ({ page }) => {
|
||||
test('desktop menus preserve ref columns, prefix-safe routes, and controlled dropdown behavior', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const state: NavigationFixture = {
|
||||
officerLevel: 5,
|
||||
permission: 2,
|
||||
@@ -1250,6 +1252,34 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
|
||||
const versionDialog = page.getByRole('dialog', { name: '게임 정보' });
|
||||
await expect(versionDialog).toBeVisible();
|
||||
await expect(versionDialog).toContainText('메인 화면 검증 시나리오');
|
||||
await expect(versionDialog.getByText('빌드 커밋', { exact: true })).toBeVisible();
|
||||
await expect(versionDialog.locator('code')).toHaveText('0123456789abcdef0123456789abcdef01234567');
|
||||
const versionGeometry = await versionDialog.evaluate((dialog) => {
|
||||
const code = dialog.querySelector('code');
|
||||
if (!code) throw new Error('game version commit is missing');
|
||||
const dialogStyle = getComputedStyle(dialog);
|
||||
const codeStyle = getComputedStyle(code);
|
||||
return {
|
||||
dialog: dialog.getBoundingClientRect().toJSON(),
|
||||
code: code.getBoundingClientRect().toJSON(),
|
||||
dialogBackground: dialogStyle.backgroundColor,
|
||||
dialogColor: dialogStyle.color,
|
||||
codeColor: codeStyle.color,
|
||||
codeFontFamily: codeStyle.fontFamily,
|
||||
viewportWidth: window.innerWidth,
|
||||
};
|
||||
});
|
||||
expect(versionGeometry.dialog.width).toBeLessThanOrEqual(versionGeometry.viewportWidth - 32);
|
||||
expect(versionGeometry.code.left).toBeGreaterThanOrEqual(versionGeometry.dialog.left);
|
||||
expect(versionGeometry.code.right).toBeLessThanOrEqual(versionGeometry.dialog.right);
|
||||
expect(versionGeometry.dialogBackground).toBe('rgb(32, 32, 32)');
|
||||
expect(versionGeometry.dialogColor).toBe('rgb(255, 255, 255)');
|
||||
expect(versionGeometry.codeColor).toBe('rgb(215, 215, 215)');
|
||||
await writeFile(
|
||||
testInfo.outputPath('desktop-game-version-dialog.json'),
|
||||
`${JSON.stringify(versionGeometry, null, 2)}\n`
|
||||
);
|
||||
await versionDialog.screenshot({ path: testInfo.outputPath('desktop-game-version-dialog.png') });
|
||||
await versionDialog.getByRole('button', { name: '닫기' }).click();
|
||||
await expect(versionDialog).toBeHidden();
|
||||
|
||||
@@ -1479,6 +1509,30 @@ test('the repeated bottom global menu opens upward on the mobile document', asyn
|
||||
expect(geometry.caretBorderTopWidth).toBe('0px');
|
||||
expect(geometry.caretBorderBottomWidth).toBe('4px');
|
||||
await bottomGlobal.screenshot({ path: testInfo.outputPath('mobile-bottom-global-dropup.png') });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await bottomGlobal.locator('[data-navigation-id="version"]').click();
|
||||
const versionDialog = page.getByRole('dialog', { name: '게임 정보' });
|
||||
await expect(versionDialog).toBeVisible();
|
||||
await expect(versionDialog.locator('code')).toHaveText('0123456789abcdef0123456789abcdef01234567');
|
||||
const versionGeometry = await versionDialog.evaluate((dialog) => {
|
||||
const code = dialog.querySelector('code');
|
||||
if (!code) throw new Error('game version commit is missing');
|
||||
return {
|
||||
dialog: dialog.getBoundingClientRect().toJSON(),
|
||||
code: code.getBoundingClientRect().toJSON(),
|
||||
viewportWidth: window.innerWidth,
|
||||
documentScrollWidth: document.documentElement.scrollWidth,
|
||||
};
|
||||
});
|
||||
expect(versionGeometry.dialog.width).toBeLessThanOrEqual(versionGeometry.viewportWidth - 32);
|
||||
expect(versionGeometry.code.left).toBeGreaterThanOrEqual(versionGeometry.dialog.left);
|
||||
expect(versionGeometry.code.right).toBeLessThanOrEqual(versionGeometry.dialog.right);
|
||||
expect(versionGeometry.documentScrollWidth).toBe(500);
|
||||
await writeFile(
|
||||
testInfo.outputPath('mobile-game-version-dialog.json'),
|
||||
`${JSON.stringify(versionGeometry, null, 2)}\n`
|
||||
);
|
||||
await versionDialog.screenshot({ path: testInfo.outputPath('mobile-game-version-dialog.png') });
|
||||
await persistArtifact(page, `${basePath.slice(1)}-mobile-bottom-dropup`);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,11 +9,12 @@ const gameProfile = process.env.PLAYWRIGHT_GAME_PROFILE ?? 'che:default';
|
||||
const baseURL = `http://127.0.0.1:${port}${basePath}/`;
|
||||
const gameApiUrl = process.env.PLAYWRIGHT_GAME_API_URL ?? `${basePath}/api/trpc`;
|
||||
const gatewayWebUrl = process.env.PLAYWRIGHT_GATEWAY_WEB_URL ?? '/gateway/';
|
||||
const buildCommitSha = process.env.PLAYWRIGHT_BUILD_COMMIT_SHA ?? '0123456789abcdef0123456789abcdef01234567';
|
||||
const useProductionBundle = process.env.PLAYWRIGHT_FRONTEND_MODE === 'production';
|
||||
const frontendEnv =
|
||||
`VITE_APP_BASE_PATH=${basePath} VITE_GAME_API_URL=${gameApiUrl} ` +
|
||||
`VITE_GAME_PROFILE=${gameProfile} VITE_GATEWAY_WEB_URL=${gatewayWebUrl} ` +
|
||||
'VITE_GATEWAY_API_URL=/gateway/api/trpc';
|
||||
`VITE_GATEWAY_API_URL=/gateway/api/trpc VITE_BUILD_COMMIT_SHA=${buildCommitSha}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: '.',
|
||||
|
||||
Vendored
+1
@@ -22,6 +22,7 @@ interface ImportMetaEnv {
|
||||
readonly VITE_BOARD_PATCH_URL?: string;
|
||||
readonly VITE_OFFICIAL_CHAT_URL?: string;
|
||||
readonly VITE_CASUAL_CHAT_URL?: string;
|
||||
readonly VITE_BUILD_COMMIT_SHA?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -44,6 +44,7 @@ const isMobile = useMediaQuery('(max-width: 939.98px)');
|
||||
const npcMode = ref(0);
|
||||
const globalNavigation = ref<MainNavigationEntry[]>(defaultGlobalNavigation);
|
||||
const versionDialog = ref<HTMLDialogElement | null>(null);
|
||||
const buildCommitSha = import.meta.env.VITE_BUILD_COMMIT_SHA?.trim() || 'unknown';
|
||||
const mobilePanelOrder = ref(loadMobileMainPanelOrder());
|
||||
const navigationUrl = (import.meta.env.VITE_GATEWAY_API_URL ?? '/api/trpc').replace(/\/trpc\/?$/u, '/navigation');
|
||||
|
||||
@@ -571,6 +572,10 @@ watch(
|
||||
<h2 id="game-version-title">게임 정보</h2>
|
||||
<p>{{ lobbyInfo?.scenarioTitle || 'Core2026' }}</p>
|
||||
<p>삼국지 모의전투 Core2026</p>
|
||||
<p class="game-version-dialog__commit">
|
||||
<span>빌드 커밋</span>
|
||||
<code>{{ buildCommitSha }}</code>
|
||||
</p>
|
||||
<form method="dialog"><button class="legacy-button legacy-button--navigation" type="submit">닫기</button></form>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -584,6 +589,7 @@ button {
|
||||
}
|
||||
|
||||
.game-version-dialog {
|
||||
box-sizing: border-box;
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
@@ -607,6 +613,18 @@ button {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.game-version-dialog__commit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.game-version-dialog__commit code {
|
||||
overflow-wrap: anywhere;
|
||||
color: #d7d7d7;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/*
|
||||
* Ref's main document does not clip horizontally; the map panel below manages
|
||||
* its own overflow.
|
||||
|
||||
@@ -28,4 +28,29 @@ void describe('game frontend Vite config', () => {
|
||||
|
||||
assert.equal(loaded?.config.build?.sourcemap, true);
|
||||
});
|
||||
|
||||
void it('uses the deployment-pinned full commit SHA as the displayed build version', async () => {
|
||||
const commitSha = 'ABCDEF0123456789ABCDEF0123456789ABCDEF01';
|
||||
const previousCommitSha = process.env.VITE_BUILD_COMMIT_SHA;
|
||||
process.env.VITE_BUILD_COMMIT_SHA = commitSha;
|
||||
try {
|
||||
const configPath = path.resolve(import.meta.dirname, '../vite.config.ts');
|
||||
const loaded = await loadConfigFromFile(
|
||||
{ command: 'build', mode: 'production' },
|
||||
configPath,
|
||||
path.dirname(configPath),
|
||||
undefined,
|
||||
undefined,
|
||||
'runner'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
loaded?.config.define?.['import.meta.env.VITE_BUILD_COMMIT_SHA'],
|
||||
JSON.stringify(commitSha.toLowerCase())
|
||||
);
|
||||
} finally {
|
||||
if (previousCommitSha === undefined) delete process.env.VITE_BUILD_COMMIT_SHA;
|
||||
else process.env.VITE_BUILD_COMMIT_SHA = previousCommitSha;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import path from 'path';
|
||||
import { mergeViteEnv } from './src/config/viteEnv';
|
||||
|
||||
const fullCommitShaPattern = /^[0-9a-f]{40,64}$/iu;
|
||||
|
||||
export const resolveBuildCommitSha = (explicitSha: string | undefined, repositoryRoot: string): string => {
|
||||
const normalizedExplicitSha = explicitSha?.trim();
|
||||
if (normalizedExplicitSha && fullCommitShaPattern.test(normalizedExplicitSha)) {
|
||||
return normalizedExplicitSha.toLowerCase();
|
||||
}
|
||||
try {
|
||||
const repositorySha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
||||
cwd: repositoryRoot,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
return fullCommitShaPattern.test(repositorySha) ? repositorySha.toLowerCase() : 'unknown';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeBasePath = (value: string | undefined): string => {
|
||||
const pathValue = (value ?? '/').trim();
|
||||
if (!pathValue || pathValue === '/') {
|
||||
@@ -27,9 +47,13 @@ const resolvePreviewAllowedHosts = (value: string | undefined): true | string[]
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = mergeViteEnv(loadEnv(mode, process.cwd(), ''), process.env);
|
||||
const buildCommitSha = resolveBuildCommitSha(env.VITE_BUILD_COMMIT_SHA, path.resolve(import.meta.dirname, '../..'));
|
||||
return {
|
||||
base: normalizeBasePath(env.VITE_APP_BASE_PATH),
|
||||
plugins: [vue(), tailwindcss()],
|
||||
define: {
|
||||
'import.meta.env.VITE_BUILD_COMMIT_SHA': JSON.stringify(buildCommitSha),
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
|
||||
@@ -552,9 +552,13 @@ const buildProfileFrontendOutDir = (workspaceRoot: string, profileName: string):
|
||||
export const buildProfileFrontendCommands = (
|
||||
workspaceRoot: string,
|
||||
profile: Pick<GatewayProfileRecord, 'profileName' | 'profile' | 'apiPort'>,
|
||||
buildCommitSha: string,
|
||||
env?: Record<string, string>,
|
||||
cacheAnchorRoot: string = workspaceRoot
|
||||
): BuildCommand[] => {
|
||||
if (!/^[0-9a-f]{40,64}$/iu.test(buildCommitSha.trim())) {
|
||||
throw new Error('Profile frontend build requires a full commit SHA.');
|
||||
}
|
||||
const profileFrontendBuildNodeOptions = env?.PROFILE_FRONTEND_BUILD_NODE_OPTIONS?.trim();
|
||||
const buildEnv = {
|
||||
...(env ?? {}),
|
||||
@@ -562,6 +566,7 @@ export const buildProfileFrontendCommands = (
|
||||
VITE_APP_BASE_PATH: `/${profile.profile}`,
|
||||
VITE_GAME_API_URL: `/${profile.profile}/api/trpc`,
|
||||
VITE_GAME_SSE_URL: `/${profile.profile}/api/events`,
|
||||
VITE_BUILD_COMMIT_SHA: buildCommitSha.trim().toLowerCase(),
|
||||
};
|
||||
return [
|
||||
buildTurboReleaseTaskCommand(
|
||||
@@ -1503,6 +1508,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
...buildProfileFrontendCommands(
|
||||
workspace.root,
|
||||
profile,
|
||||
commitSha,
|
||||
this.processConfig.baseEnv,
|
||||
this.processConfig.workspaceRoot
|
||||
),
|
||||
@@ -2090,6 +2096,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
? buildProfileFrontendCommands(
|
||||
workspace.root,
|
||||
profile,
|
||||
commitSha,
|
||||
this.processConfig.baseEnv,
|
||||
this.processConfig.workspaceRoot
|
||||
)
|
||||
|
||||
@@ -371,9 +371,11 @@ describe('buildWorkspaceCommands', () => {
|
||||
});
|
||||
|
||||
describe('buildProfileFrontendCommands', () => {
|
||||
const buildCommitSha = '0123456789abcdef0123456789abcdef01234567';
|
||||
|
||||
it('uses a profile frontend build-only Node heap without changing the shared runtime heap', () => {
|
||||
const workspaceRoot = '/srv/sammo/worktrees/0123456789abcdef';
|
||||
const commands = buildProfileFrontendCommands(workspaceRoot, buildProfile(), {
|
||||
const commands = buildProfileFrontendCommands(workspaceRoot, buildProfile(), buildCommitSha, {
|
||||
NODE_OPTIONS: '--max-old-space-size=1536',
|
||||
PROFILE_FRONTEND_BUILD_NODE_OPTIONS: '--max-old-space-size=2048',
|
||||
});
|
||||
@@ -385,6 +387,7 @@ describe('buildProfileFrontendCommands', () => {
|
||||
(command) => command.env?.PROFILE_FRONTEND_BUILD_NODE_OPTIONS === '--max-old-space-size=2048'
|
||||
)
|
||||
).toBe(true);
|
||||
expect(commands.every((command) => command.env?.VITE_BUILD_COMMIT_SHA === buildCommitSha)).toBe(true);
|
||||
expect(commands[0]?.args).toEqual([
|
||||
'exec',
|
||||
'turbo',
|
||||
@@ -401,10 +404,16 @@ describe('buildProfileFrontendCommands', () => {
|
||||
|
||||
it('keeps the shared Node heap when no frontend build override is configured', () => {
|
||||
const workspaceRoot = '/srv/sammo/worktrees/0123456789abcdef';
|
||||
const commands = buildProfileFrontendCommands(workspaceRoot, buildProfile(), {
|
||||
const commands = buildProfileFrontendCommands(workspaceRoot, buildProfile(), buildCommitSha, {
|
||||
NODE_OPTIONS: '--max-old-space-size=1536',
|
||||
});
|
||||
|
||||
expect(commands.every((command) => command.env?.NODE_OPTIONS === '--max-old-space-size=1536')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a non-commit build version before creating cached frontend commands', () => {
|
||||
expect(() => buildProfileFrontendCommands('/srv/sammo/worktrees/main', buildProfile(), 'main')).toThrow(
|
||||
'Profile frontend build requires a full commit SHA.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,6 +194,8 @@ describe('profile DEPLOY operation', () => {
|
||||
'tools/build-scripts/materialize-profile-frontend.mjs',
|
||||
'che:1010',
|
||||
]);
|
||||
expect(commandGroups[0]?.[2]?.env?.VITE_BUILD_COMMIT_SHA).toBe(SHA);
|
||||
expect(commandGroups[0]?.[3]?.env?.VITE_BUILD_COMMIT_SHA).toBe(SHA);
|
||||
expect(commandGroups[1]?.map((command) => command.args)).toEqual([
|
||||
['--filter', '@sammo-ts/infra', 'prisma:migrate:deploy:game'],
|
||||
]);
|
||||
|
||||
@@ -125,6 +125,12 @@ commit의 game API, engine과 profile 전용 frontend artifact를 빌드합니
|
||||
프로세스를 멈춘 뒤 `prisma migrate deploy`만 실행하고 seed는 호출하지 않습니다.
|
||||
새 API·frontend와 모든 worker가 PM2 `online`이고 HTTP readiness가 성공해야
|
||||
build commit을 게시합니다. 실패하면 이전 worktree 프로세스를 다시 시작합니다.
|
||||
Profile frontend build에는 같은 전체 commit SHA를 `VITE_BUILD_COMMIT_SHA`로
|
||||
주입합니다. 이 값은 Turbo의 `VITE_*` cache key에 포함되고 Vite가 bundle 상수로
|
||||
고정하므로, 게임의 `게임 정보` dialog가 실제 선택 build commit을 표시하며 다른
|
||||
commit의 cached artifact를 현재 버전으로 오인하지 않습니다. Orchestrator 밖의
|
||||
개발 build는 현재 Git checkout의 `HEAD`를 fallback으로 사용하고 Git metadata를
|
||||
읽을 수 없을 때만 `unknown`을 표시합니다.
|
||||
`RESET` operation은 같은 build 경계를 사용한 뒤 현재 시즌 테이블을 seed로
|
||||
교체합니다. Seeder의 reset 목록에는 `hall`, `ng_games`, `yearbook_history`,
|
||||
과거 장수·국가와 상속·진단 자료가 포함되지 않습니다.
|
||||
|
||||
Reference in New Issue
Block a user