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);