From 6bf8340a8a486788aadbfb313adb45ac8f61f577 Mon Sep 17 00:00:00 2001 From: hided62 Date: Tue, 11 Aug 2026 00:00:02 +0000 Subject: [PATCH 1/2] fix(release): fetch remote commit before self-upgrade --- .../src/orchestrator/workspaceManager.ts | 23 +++++++++++++++---- app/gateway-api/test/workspaceManager.test.ts | 17 ++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app/gateway-api/src/orchestrator/workspaceManager.ts b/app/gateway-api/src/orchestrator/workspaceManager.ts index 931bb1ca..be1fc191 100644 --- a/app/gateway-api/src/orchestrator/workspaceManager.ts +++ b/app/gateway-api/src/orchestrator/workspaceManager.ts @@ -77,12 +77,25 @@ export class GitWorkspaceManager { sourceMode === 'BRANCH' ? [`refs/remotes/origin/${ref}^{commit}`, `refs/heads/${ref}^{commit}`] : [`${ref}^{commit}`]; - for (const candidate of candidates) { - const result = await runGit(['rev-parse', '--verify', candidate], this.repoRoot, this.baseEnv); - const commitSha = result.output.trim().split('\n')[0]; - if (result.ok && /^[0-9a-f]{40}$/i.test(commitSha)) { - return commitSha; + const resolveCandidates = async (): Promise => { + for (const candidate of candidates) { + const result = await runGit(['rev-parse', '--verify', candidate], this.repoRoot, this.baseEnv); + const commitSha = result.output.trim().split('\n')[0]; + if (result.ok && /^[0-9a-f]{40}$/i.test(commitSha)) { + return commitSha; + } } + return undefined; + }; + const localCommit = await resolveCandidates(); + if (localCommit) return localCommit; + if (sourceMode === 'COMMIT') { + const fetched = await runGit(['fetch', '--all', '--tags'], this.repoRoot, this.baseEnv); + if (!fetched.ok) { + throw new Error(fetched.output || 'Failed to fetch git commits.'); + } + const fetchedCommit = await resolveCandidates(); + if (fetchedCommit) return fetchedCommit; } throw new Error(`${sourceMode === 'BRANCH' ? 'Branch' : 'Commit'} not found.`); } diff --git a/app/gateway-api/test/workspaceManager.test.ts b/app/gateway-api/test/workspaceManager.test.ts index e1a16854..bf0a50f2 100644 --- a/app/gateway-api/test/workspaceManager.test.ts +++ b/app/gateway-api/test/workspaceManager.test.ts @@ -75,6 +75,23 @@ describe('GitWorkspaceManager source resolution', () => { expect(await manager.resolveCommit('BRANCH', 'main')).toBe(secondCommit); }); + it('fetches a remote commit that is not present in the controller checkout yet', async () => { + const fixture = createRepositoryFixture(); + const manager = new GitWorkspaceManager({ + repoRoot: fixture.checkout, + worktreeRoot: fixture.worktrees, + }); + + fs.writeFileSync(path.join(fixture.source, 'version.txt'), 'remote-only\n'); + git(fixture.source, 'add', 'version.txt'); + git(fixture.source, 'commit', '-m', 'remote only'); + const remoteCommit = git(fixture.source, 'rev-parse', 'HEAD'); + git(fixture.source, 'push', 'origin', 'main'); + expect(() => git(fixture.checkout, 'cat-file', '-e', `${remoteCommit}^{commit}`)).toThrow(); + + await expect(manager.resolveCommit('COMMIT', remoteCommit)).resolves.toBe(remoteCommit); + }); + it('rejects option-like and range refs', async () => { const fixture = createRepositoryFixture(); const manager = new GitWorkspaceManager({ From 0c8185cfa939da94a52b5c663116a30f5555c81f Mon Sep 17 00:00:00 2001 From: hided62 Date: Tue, 11 Aug 2026 00:01:47 +0000 Subject: [PATCH 2/2] fix(release): allow controller protocol self-upgrade --- .../src/orchestrator/releaseManifest.ts | 7 +++++-- app/gateway-api/test/releaseManifest.test.ts | 20 +++++++++++++++++-- app/release-controller/README.md | 4 +++- app/release-controller/src/selfUpgrade.ts | 5 ++++- docs/release-operations.md | 2 ++ 5 files changed, 32 insertions(+), 6 deletions(-) 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 검사는 그대로 수행합니다. ## 운영 확인 목록