fix: 첫 빌드 전 닫힌 서버도 패치 정책으로 오픈 요청을 받는다

This commit is contained in:
2026-10-23 00:42:11 +09:00
parent d4a87c981f
commit b377250fb9
5 changed files with 51 additions and 10 deletions
@@ -0,0 +1,26 @@
import { TRPCError } from '@trpc/server';
import type { GatewayProfileRecord } from '../orchestrator/profileRepository.js';
import { readProfileReleaseSource } from '../orchestrator/profileReleaseSource.js';
import { resolveGitBranchCommitSha, resolveGitCommitSha } from '../scenario/scenarioCatalog.js';
// 이미 배포된 서버는 그 catalog를 보존하고, 첫 오픈 전 서버만 운영자가 정한 source를 사용한다.
// 일반 사용자는 이 조회에도 branch/commit을 전달할 수 없다.
export const resolveOpeningCatalogCommit = async (
profile: Pick<GatewayProfileRecord, 'meta' | 'buildCommitSha'>
): Promise<string> => {
if (profile.buildCommitSha?.trim()) return profile.buildCommitSha.trim();
const source = readProfileReleaseSource(profile);
if (!source) {
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '서버의 배포 버전 또는 패치 정책이 필요합니다.' });
}
try {
return source.mode === 'BRANCH'
? await resolveGitBranchCommitSha(source.ref)
: await resolveGitCommitSha(source.ref);
} catch {
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: '서버 패치 정책의 시나리오 버전을 확인하지 못했습니다.',
});
}
};
+6 -2
View File
@@ -16,6 +16,7 @@ import {
} from '../scenario/scenarioCatalog.js';
import { canRequestOpening, openingSeasonKey, readOpenRequestPolicy, type OpenRequestPolicy } from './policy.js';
import { z } from 'zod';
import { resolveOpeningCatalogCommit } from './catalogSource.js';
export const zOpeningInstall = zOperationInstallOptions.required({ preopenAt: true, openAt: true }).strict();
export type OpeningInstall = z.infer<typeof zOpeningInstall>;
@@ -200,8 +201,11 @@ export const createOpenRequest = async (
}
if (old.filter((request) => activeStatus(request, profile, now) === 'PENDING').length >= 100)
return fail('이 서버의 대기 요청이 너무 많습니다.', 'CONFLICT');
if (!profile.buildCommitSha) return fail('서버의 시나리오 버전이 준비되지 않았습니다.');
const scenario = (await listScenarioPreviews({ gitRef: profile.buildCommitSha })).find(
const catalogCommit = await resolveOpeningCatalogCommit({
meta: profile.meta as GatewayPrisma.JsonObject,
buildCommitSha: profile.buildCommitSha ?? undefined,
});
const scenario = (await listScenarioPreviews({ gitRef: catalogCommit })).find(
(entry) => entry.id === install.scenarioId
);
if (!scenario) return fail('현재 서버에 없는 시나리오입니다.');
+4 -8
View File
@@ -12,6 +12,7 @@ import { toPublicUser } from './auth/userRepository.js';
import type { UserOAuthInfo, UserRecord } from './auth/userRepository.js';
import { adminRouter } from './adminRouter.js';
import { openRequestsRouter } from './openRequests/router.js';
import { resolveOpeningCatalogCommit } from './openRequests/catalogSource.js';
import { accountRouter } from './account/router.js';
import {
hasActiveSpecialAccountGrant,
@@ -217,16 +218,11 @@ export const appRouter = router({
throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found.' });
}
const profile = await ctx.profiles.getProfile(input.profileName);
const activeBuildCommit = profile?.buildCommitSha?.trim();
if (!profile || !activeBuildCommit) {
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: 'The profile has no active build commit.',
});
}
if (!profile) throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found.' });
const catalogCommit = await resolveOpeningCatalogCommit(profile);
try {
return await listScenarioPreviews({ gitRef: activeBuildCommit });
return await listScenarioPreviews({ gitRef: catalogCommit });
} catch {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
@@ -209,6 +209,17 @@ suite('durable opening requests (isolated PostgreSQL and HTTP)', () => {
expect((await listOpenRequests(context(), await user(2), profileName)).requests).toHaveLength(1);
expect(JSON.stringify(await listOpenRequests(context(), await user(2), profileName))).not.toContain('password');
});
it('supports the first opening of a closed server using its configured source without an active build', async () => {
await prisma.gatewayProfile.update({
where: { profileName },
data: { buildCommitSha: null, meta: { releaseSource: { mode: 'BRANCH', ref: 'main' } } },
});
expect(await (await caller(1)).lobby.scenarios({ profileName })).toHaveLength(1);
expect(catalog.resolveGitBranchCommitSha).toHaveBeenCalledWith('main');
const request = await create();
await (await caller(3)).admin.openRequests.decide({ id: request.id, action: 'ACCEPT' });
expect((await prisma.gatewayOperation.findFirstOrThrow({ where: { profileName } })).sourceMode).toBe('BRANCH');
});
it('uses the configured lifetime for distant proposals', async () => {
await policy(null, { lifetimeDays: 2 });
const input = install();