diff --git a/app/gateway-api/src/orchestrator/releaseManifest.ts b/app/gateway-api/src/orchestrator/releaseManifest.ts index a2c77670..ffee0ead 100644 --- a/app/gateway-api/src/orchestrator/releaseManifest.ts +++ b/app/gateway-api/src/orchestrator/releaseManifest.ts @@ -31,7 +31,10 @@ const assertMigrationHead = async (workspaceRoot: string, directory: string, exp } }; -export const readReleaseManifest = async (workspaceRoot: string): Promise => { +export const readReleaseManifest = async ( + workspaceRoot: string, + options: { allowControllerUpgrade?: boolean } = {} +): Promise => { const manifestPath = path.join(workspaceRoot, 'release-manifest.json'); const parsed = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as unknown; if ( @@ -46,7 +49,7 @@ export const readReleaseManifest = async (workspaceRoot: string): Promise RELEASE_CONTROLLER_PROTOCOL) { + if (parsed.controllerProtocol > RELEASE_CONTROLLER_PROTOCOL && !options.allowControllerUpgrade) { throw new Error( `Release requires controller protocol ${parsed.controllerProtocol}; this controller supports ${RELEASE_CONTROLLER_PROTOCOL}.` ); diff --git a/app/gateway-api/test/releaseManifest.test.ts b/app/gateway-api/test/releaseManifest.test.ts index f1b6afaf..3d572495 100644 --- a/app/gateway-api/test/releaseManifest.test.ts +++ b/app/gateway-api/test/releaseManifest.test.ts @@ -8,7 +8,7 @@ import { readReleaseManifest, RELEASE_CONTROLLER_PROTOCOL } from '../src/orchest const temporaryDirectories: string[] = []; -const createWorkspace = async (gatewayHead: string, gameHead: string): Promise => { +const createWorkspace = async (gatewayHead: string, gameHead: string, controllerProtocol = 1): Promise => { const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'sammo-release-manifest-')); temporaryDirectories.push(workspace); await fs.mkdir(path.join(workspace, 'packages/infra/prisma/gateway-migrations', gatewayHead), { @@ -19,7 +19,7 @@ const createWorkspace = async (gatewayHead: string, gameHead: string): Promise { await expect(readReleaseManifest(workspace)).rejects.toThrow('does not match workspace head'); }); + + it('allows only the explicit controller self-upgrade boundary to cross protocol versions', async () => { + const futureProtocol = RELEASE_CONTROLLER_PROTOCOL + 1; + const workspace = await createWorkspace( + '20260801000000_gateway', + '20260801000000_game', + futureProtocol + ); + + await expect(readReleaseManifest(workspace)).rejects.toThrow( + `Release requires controller protocol ${futureProtocol}` + ); + await expect(readReleaseManifest(workspace, { allowControllerUpgrade: true })).resolves.toMatchObject({ + controllerProtocol: futureProtocol, + }); + }); }); diff --git a/app/release-controller/README.md b/app/release-controller/README.md index 3d42fda2..05d895c2 100644 --- a/app/release-controller/README.md +++ b/app/release-controller/README.md @@ -86,4 +86,6 @@ rollback하려면 새 schema와의 하위 호환성을 릴리스 전에 확인 `release-manifest.json`의 `controllerProtocol`이 올라간 릴리스는 controller를 먼저 self-upgrade해야 합니다. Protocol 2는 `GatewayReleaseLog` 진행 로그 저장을 요구합니다. 구형 controller로 새 Gateway만 배포하면 관리자 화면과 controller의 -기능이 어긋날 수 있으므로, manifest protocol 검사를 우회하지 마세요. +기능이 어긋날 수 있으므로, 일반 배포의 manifest protocol 검사를 우회하지 +마세요. Self-upgrade CLI만 다음 protocol을 허용하며 schema head와 component는 +동일하게 검증합니다. diff --git a/app/release-controller/src/selfUpgrade.ts b/app/release-controller/src/selfUpgrade.ts index 22dcb51b..9f5cdfe8 100644 --- a/app/release-controller/src/selfUpgrade.ts +++ b/app/release-controller/src/selfUpgrade.ts @@ -65,7 +65,10 @@ export const upgradeReleaseController = async (options: { }): Promise<{ commitSha: string; workspace: string }> => { const commitSha = await options.workspaceManager.resolveCommit(options.sourceMode, options.sourceRef); const workspace = await options.workspaceManager.prepare(commitSha); - const manifest = await readReleaseManifest(workspace.root); + // The target controller, rather than this bootstrap CLI, owns the target + // controller protocol. Keep all manifest/schema/component checks while + // allowing this explicit self-upgrade boundary to cross protocol versions. + const manifest = await readReleaseManifest(workspace.root, { allowControllerUpgrade: true }); assertReleaseComponents(manifest, ['release-controller']); const build = await options.buildRunner.run( buildReleaseControllerCommands(workspace.root, workspace.needsInstall, options.config) diff --git a/docs/release-operations.md b/docs/release-operations.md index b4c64b28..7bd570fd 100644 --- a/docs/release-operations.md +++ b/docs/release-operations.md @@ -204,6 +204,8 @@ release-controller가 `GatewayReleaseLog` 진행 로그를 저장하는 것이 로그 기능이 포함된 Gateway API/frontend만 먼저 배포하면 화면은 polling하지만 구형 controller는 로그를 만들 수 있으므로, protocol 변경 commit은 위 `self-upgrade`로 controller를 먼저 전환한 뒤 Gateway 배포를 요청해야 합니다. +명시적인 self-upgrade 경로만 다음 controller protocol의 manifest를 읽을 수 있고, +schema head·component 검사는 그대로 수행합니다. ## 운영 확인 목록