From 4bf812b60176ef142bcaa30e058ff53ab2a6ae8a Mon Sep 17 00:00:00 2001 From: hided62 Date: Thu, 24 Sep 2026 05:47:46 +0000 Subject: [PATCH] =?UTF-8?q?=EC=98=88=EC=95=BD=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=EC=97=90=EC=84=9C=20=EA=B5=AC=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=8A=A4=20=EB=B9=8C=EB=93=9C=20=ED=98=B8?= =?UTF-8?q?=ED=99=98=EC=84=B1=EC=9D=84=20=ED=99=95=EC=9D=B8=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/gateway-api/src/adminRouter.ts | 10 ++++++ .../src/scenario/scenarioCatalog.ts | 31 +++++++++++++++++++ app/gateway-api/test/scenarioCatalog.test.ts | 13 +++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/app/gateway-api/src/adminRouter.ts b/app/gateway-api/src/adminRouter.ts index d57875bb..b78cc444 100644 --- a/app/gateway-api/src/adminRouter.ts +++ b/app/gateway-api/src/adminRouter.ts @@ -11,6 +11,7 @@ import { listScenarioPreviews, resolveGitBranchCommitSha, resolveGitCommitSha, + supportsProfileReleaseBuild, type ScenarioPreview, } from './scenario/scenarioCatalog.js'; import type { UserSanctions, UserServerRestriction } from './auth/userRepository.js'; @@ -1373,6 +1374,7 @@ export const adminRouter = router({ }); } let selectedScenario: ScenarioPreview | undefined; + let resolvedCommitSha: string; try { const resolved = sourceMode === 'BRANCH' @@ -1381,6 +1383,7 @@ export const adminRouter = router({ if (sourceMode === 'COMMIT') { sourceRef = resolved; } + resolvedCommitSha = resolved; const scenarios = await listScenarioPreviews({ gitRef: resolved }); selectedScenario = scenarios.find((scenario) => scenario.id === input.install.scenarioId); if (!selectedScenario) { @@ -1396,6 +1399,13 @@ export const adminRouter = router({ }); } + if (!(await supportsProfileReleaseBuild(resolvedCommitSha))) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: '선택한 버전은 현재 서버의 배포 빌드와 호환되지 않습니다. 최신 버전을 선택해 주세요.', + }); + } + try { const operation = await ctx.profiles.createOperation({ profileName: input.profileName, diff --git a/app/gateway-api/src/scenario/scenarioCatalog.ts b/app/gateway-api/src/scenario/scenarioCatalog.ts index 14315ddd..53ddf921 100644 --- a/app/gateway-api/src/scenario/scenarioCatalog.ts +++ b/app/gateway-api/src/scenario/scenarioCatalog.ts @@ -137,6 +137,37 @@ const readGitJson = async (commitSha: string, relativePath: string): Promise { + if (!value || typeof value !== 'object' || !('scripts' in value)) return false; + const scripts = value.scripts; + return Boolean(scripts && typeof scripts === 'object' && name in scripts && typeof scripts[name as keyof typeof scripts] === 'string'); +}; + +export const hasProfileReleaseBuildTasks = (turboConfig: unknown, frontendPackage: unknown): boolean => { + if (!turboConfig || typeof turboConfig !== 'object' || !('tasks' in turboConfig)) return false; + const tasks = turboConfig.tasks; + return Boolean( + tasks && + typeof tasks === 'object' && + 'typecheck:release' in tasks && + 'build:release' in tasks && + hasScript(frontendPackage, 'typecheck:release') && + hasScript(frontendPackage, 'build:release') + ); +}; + +export const supportsProfileReleaseBuild = async (commitSha: string): Promise => { + try { + const [turboConfig, frontendPackage] = await Promise.all([ + readGitJson(commitSha, 'turbo.json'), + readGitJson(commitSha, 'app/game-frontend/package.json'), + ]); + return hasProfileReleaseBuildTasks(turboConfig, frontendPackage); + } catch { + return false; + } +}; + const listScenarioIds = async (): Promise => { const root = resolveScenarioRoot(); const entries = await fs.readdir(root, { withFileTypes: true }); diff --git a/app/gateway-api/test/scenarioCatalog.test.ts b/app/gateway-api/test/scenarioCatalog.test.ts index c3efe05b..dc667c29 100644 --- a/app/gateway-api/test/scenarioCatalog.test.ts +++ b/app/gateway-api/test/scenarioCatalog.test.ts @@ -1,8 +1,19 @@ import { describe, expect, it } from 'vitest'; -import { listScenarioPreviews, resolveGitCommitSha } from '../src/scenario/scenarioCatalog.js'; +import { + hasProfileReleaseBuildTasks, + listScenarioPreviews, + resolveGitCommitSha, + supportsProfileReleaseBuild, +} from '../src/scenario/scenarioCatalog.js'; describe('scenarioCatalog git ref support', () => { + it('rejects a pinned release whose frontend predates the current build contract', async () => { + expect(hasProfileReleaseBuildTasks({ tasks: { build: {} } }, { scripts: { build: 'vite build' } })).toBe( + false + ); + expect(await supportsProfileReleaseBuild('HEAD')).toBe(true); + }); it.each([undefined, 'HEAD'])('publishes resolved start years for every scenario (%s)', async (gitRef) => { const previews = await listScenarioPreviews({ gitRef }); expect(previews.every((scenario) => Number.isFinite(scenario.year))).toBe(true);