예약 초기화에서 구버전 릴리스 빌드 호환성을 확인한다

This commit is contained in:
2026-09-24 05:47:46 +00:00
parent 3c75de16e9
commit 4bf812b601
3 changed files with 53 additions and 1 deletions
+10
View File
@@ -11,6 +11,7 @@ import {
listScenarioPreviews, listScenarioPreviews,
resolveGitBranchCommitSha, resolveGitBranchCommitSha,
resolveGitCommitSha, resolveGitCommitSha,
supportsProfileReleaseBuild,
type ScenarioPreview, type ScenarioPreview,
} from './scenario/scenarioCatalog.js'; } from './scenario/scenarioCatalog.js';
import type { UserSanctions, UserServerRestriction } from './auth/userRepository.js'; import type { UserSanctions, UserServerRestriction } from './auth/userRepository.js';
@@ -1373,6 +1374,7 @@ export const adminRouter = router({
}); });
} }
let selectedScenario: ScenarioPreview | undefined; let selectedScenario: ScenarioPreview | undefined;
let resolvedCommitSha: string;
try { try {
const resolved = const resolved =
sourceMode === 'BRANCH' sourceMode === 'BRANCH'
@@ -1381,6 +1383,7 @@ export const adminRouter = router({
if (sourceMode === 'COMMIT') { if (sourceMode === 'COMMIT') {
sourceRef = resolved; sourceRef = resolved;
} }
resolvedCommitSha = resolved;
const scenarios = await listScenarioPreviews({ gitRef: resolved }); const scenarios = await listScenarioPreviews({ gitRef: resolved });
selectedScenario = scenarios.find((scenario) => scenario.id === input.install.scenarioId); selectedScenario = scenarios.find((scenario) => scenario.id === input.install.scenarioId);
if (!selectedScenario) { if (!selectedScenario) {
@@ -1396,6 +1399,13 @@ export const adminRouter = router({
}); });
} }
if (!(await supportsProfileReleaseBuild(resolvedCommitSha))) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: '선택한 버전은 현재 서버의 배포 빌드와 호환되지 않습니다. 최신 버전을 선택해 주세요.',
});
}
try { try {
const operation = await ctx.profiles.createOperation({ const operation = await ctx.profiles.createOperation({
profileName: input.profileName, profileName: input.profileName,
@@ -137,6 +137,37 @@ const readGitJson = async (commitSha: string, relativePath: string): Promise<unk
return JSON.parse(raw) as unknown; return JSON.parse(raw) as unknown;
}; };
const hasScript = (value: unknown, name: string): boolean => {
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<boolean> => {
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<number[]> => { const listScenarioIds = async (): Promise<number[]> => {
const root = resolveScenarioRoot(); const root = resolveScenarioRoot();
const entries = await fs.readdir(root, { withFileTypes: true }); const entries = await fs.readdir(root, { withFileTypes: true });
+12 -1
View File
@@ -1,8 +1,19 @@
import { describe, expect, it } from 'vitest'; 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', () => { 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) => { it.each([undefined, 'HEAD'])('publishes resolved start years for every scenario (%s)', async (gitRef) => {
const previews = await listScenarioPreviews({ gitRef }); const previews = await listScenarioPreviews({ gitRef });
expect(previews.every((scenario) => Number.isFinite(scenario.year))).toBe(true); expect(previews.every((scenario) => Number.isFinite(scenario.year))).toBe(true);