From 6a333cdf05708060ff45c1ad0d47282dc620605b Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 8 Aug 2026 15:57:38 +0000 Subject: [PATCH] fix: preserve Gateway release runtime context --- app/release-controller/README.md | 7 +++++ app/release-controller/src/config.ts | 8 +++++- .../src/releaseController.ts | 9 ++++++- app/release-controller/src/selfUpgrade.ts | 2 +- .../test/releaseController.test.ts | 27 ++++++++++++++++++- docs/release-operations.md | 9 ++++++- 6 files changed, 57 insertions(+), 5 deletions(-) diff --git a/app/release-controller/README.md b/app/release-controller/README.md index fc5ef6e6..b3db1ea9 100644 --- a/app/release-controller/README.md +++ b/app/release-controller/README.md @@ -21,6 +21,7 @@ Gateway process 환경에 전달하지 않습니다. 이 값이 frontend 정의 ## 환경 변수 - `GATEWAY_DATABASE_URL`: Gateway PostgreSQL URL입니다. 필수입니다. +- `REDIS_URL`: Gateway API와 orchestrator가 사용할 Redis URL입니다. 필수입니다. - `GATEWAY_DB_SCHEMA`: Gateway schema이며 기본값은 `public`입니다. - `RELEASE_CONTROLLER_WORKSPACE_ROOT`: Git checkout입니다. - `RELEASE_CONTROLLER_WORKTREE_ROOT`: commit worktree 상위 경로입니다. @@ -32,6 +33,12 @@ Gateway process 환경에 전달하지 않습니다. 이 값이 frontend 정의 비밀값은 Git에서 제외된 환경 파일 또는 process 환경으로 전달해 주세요. `VITE_*`에는 공개 URL만 넣어 주세요. +Self-upgrade는 controller의 script와 cwd만 선택 commit worktree로 바꿉니다. +`RELEASE_CONTROLLER_WORKSPACE_ROOT`는 원래 Git checkout을 유지해야 합니다. 이를 +controller artifact worktree로 바꾸면 아직 게시된 release state가 없는 최초 +DEPLOY의 rollback이 frontend build가 없는 controller worktree를 이전 Gateway로 +오인할 수 있습니다. + ## 설치와 실행 먼저 controller가 읽을 Gateway schema를 migration하고 의존 package를 함께 diff --git a/app/release-controller/src/config.ts b/app/release-controller/src/config.ts index 6e6e81d5..1d0c2f99 100644 --- a/app/release-controller/src/config.ts +++ b/app/release-controller/src/config.ts @@ -33,6 +33,9 @@ export const resolveReleaseControllerConfig = (env: NodeJS.ProcessEnv = process. if (!rawGatewayDatabaseUrl) { throw new Error('GATEWAY_DATABASE_URL or DATABASE_URL is required.'); } + if (!env.REDIS_URL?.trim()) { + throw new Error('REDIS_URL is required.'); + } const workspaceRoot = path.resolve(env.RELEASE_CONTROLLER_WORKSPACE_ROOT ?? process.cwd()); const gatewayDbSchema = env.GATEWAY_DB_SCHEMA?.trim() || 'public'; return { @@ -51,6 +54,9 @@ export const resolveReleaseControllerConfig = (env: NodeJS.ProcessEnv = process. 60000, 'RELEASE_CONTROLLER_READINESS_TIMEOUT_MS' ), - baseEnv: sanitizeManagedProcessEnv(env), + baseEnv: { + ...sanitizeManagedProcessEnv(env), + REDIS_URL: env.REDIS_URL.trim(), + }, }; }; diff --git a/app/release-controller/src/releaseController.ts b/app/release-controller/src/releaseController.ts index 4ececf8d..e6d88456 100644 --- a/app/release-controller/src/releaseController.ts +++ b/app/release-controller/src/releaseController.ts @@ -59,12 +59,17 @@ export const buildGatewayProcessDefinitions = ( workspaceRoot: string, config: ReleaseControllerConfig ): ProcessDefinition[] => { + const redisUrl = config.baseEnv.REDIS_URL?.trim(); + if (!redisUrl) { + throw new Error('REDIS_URL is required to start Gateway processes.'); + } const apiCwd = path.join(workspaceRoot, 'app', 'gateway-api'); const frontendCwd = path.join(workspaceRoot, 'app', 'gateway-frontend'); const apiScript = path.join(apiCwd, 'dist', 'index.js'); const frontendScript = path.join(frontendCwd, 'node_modules', 'vite', 'bin', 'vite.js'); const env = { ...sanitizeManagedProcessEnv(config.baseEnv), + REDIS_URL: redisUrl, GATEWAY_API_HOST: '0.0.0.0', GATEWAY_API_PORT: String(config.gatewayApiPort), GATEWAY_DATABASE_URL: config.gatewayDatabaseUrl, @@ -232,7 +237,9 @@ export class GatewayReleaseController { try { const [api, frontend] = await Promise.all([this.fetchImpl(apiUrl), this.fetchImpl(frontendUrl)]); const processes = await this.processManager.list(); - const expected = processes.filter((process) => PROCESS_NAMES.includes(process.name as (typeof PROCESS_NAMES)[number])); + const expected = processes.filter((process) => + PROCESS_NAMES.includes(process.name as (typeof PROCESS_NAMES)[number]) + ); const safe = expected.filter( (process) => process.status.toLowerCase() === 'online' && (process.restartCount ?? 0) === 0 ); diff --git a/app/release-controller/src/selfUpgrade.ts b/app/release-controller/src/selfUpgrade.ts index 76aae283..22dcb51b 100644 --- a/app/release-controller/src/selfUpgrade.ts +++ b/app/release-controller/src/selfUpgrade.ts @@ -46,7 +46,7 @@ export const buildReleaseControllerDefinition = ( ...sanitizeManagedProcessEnv(config.baseEnv), GATEWAY_DATABASE_URL: config.gatewayDatabaseUrl, GATEWAY_DB_SCHEMA: config.gatewayDbSchema, - RELEASE_CONTROLLER_WORKSPACE_ROOT: workspaceRoot, + RELEASE_CONTROLLER_WORKSPACE_ROOT: config.workspaceRoot, RELEASE_CONTROLLER_WORKTREE_ROOT: config.worktreeRoot, }, }); diff --git a/app/release-controller/test/releaseController.test.ts b/app/release-controller/test/releaseController.test.ts index 78c65858..975c0907 100644 --- a/app/release-controller/test/releaseController.test.ts +++ b/app/release-controller/test/releaseController.test.ts @@ -73,7 +73,9 @@ const config: ReleaseControllerConfig = { gatewayBasePath: '/gateway', pollIntervalMs: 5, readinessTimeoutMs: 10, - baseEnv: {}, + baseEnv: { + REDIS_URL: 'redis://integration.invalid:6379/0', + }, }; afterEach(async () => { @@ -127,6 +129,7 @@ it('does not forward release-controller PM2 identity to Gateway processes', () = ...config, baseEnv: { DATABASE_URL: 'postgresql://integration.invalid/sammo', + REDIS_URL: 'redis://integration.invalid:6379/0', GATEWAY_ROLE: 'orchestrator', args: 'daemon', name: 'sammo:release-controller', @@ -144,6 +147,15 @@ it('does not forward release-controller PM2 identity to Gateway processes', () = } }); +it('rejects Gateway definitions before switching processes when Redis connection context is missing', () => { + expect(() => + buildGatewayProcessDefinitions('/srv/sammo/release', { + ...config, + baseEnv: {}, + }) + ).toThrow('REDIS_URL is required to start Gateway processes.'); +}); + describe('GatewayReleaseController', () => { it('builds, migrates, switches all gateway roles, verifies readiness, and publishes atomically', async () => { const workspace = await createReleaseWorkspace(); @@ -239,6 +251,7 @@ describe('resolveReleaseControllerConfig', () => { it('applies the configured gateway schema to the controller database URL', () => { const resolved = resolveReleaseControllerConfig({ GATEWAY_DATABASE_URL: 'postgresql://user:pass@127.0.0.1:5432/sammo?schema=wrong', + REDIS_URL: 'redis://127.0.0.1:6379/0', GATEWAY_DB_SCHEMA: 'gateway_release', RELEASE_CONTROLLER_WORKSPACE_ROOT: '/srv/sammo/controller', }); @@ -249,6 +262,7 @@ describe('resolveReleaseControllerConfig', () => { it('removes PM2 metadata from the inherited controller environment', () => { const resolved = resolveReleaseControllerConfig({ GATEWAY_DATABASE_URL: 'postgresql://user:pass@127.0.0.1:5432/sammo', + REDIS_URL: 'redis://127.0.0.1:6379/0', RELEASE_CONTROLLER_WORKSPACE_ROOT: '/srv/sammo/controller', args: 'daemon', pm_id: '3', @@ -258,6 +272,7 @@ describe('resolveReleaseControllerConfig', () => { expect(resolved.baseEnv).toMatchObject({ GATEWAY_DATABASE_URL: 'postgresql://user:pass@127.0.0.1:5432/sammo', + REDIS_URL: 'redis://127.0.0.1:6379/0', RELEASE_CONTROLLER_WORKSPACE_ROOT: '/srv/sammo/controller', }); expect(resolved.baseEnv).not.toHaveProperty('pm_id'); @@ -265,6 +280,15 @@ describe('resolveReleaseControllerConfig', () => { expect(resolved.baseEnv).not.toHaveProperty('name'); expect(resolved.baseEnv).not.toHaveProperty('axm_monitor'); }); + + it('rejects a controller environment that cannot start Redis-backed Gateway processes', () => { + expect(() => + resolveReleaseControllerConfig({ + GATEWAY_DATABASE_URL: 'postgresql://user:pass@127.0.0.1:5432/sammo', + RELEASE_CONTROLLER_WORKSPACE_ROOT: '/srv/sammo/controller', + }) + ).toThrow('REDIS_URL is required.'); + }); }); describe('upgradeReleaseController', () => { @@ -280,6 +304,7 @@ describe('upgradeReleaseController', () => { }); expect(definition.env).toMatchObject({ DATABASE_URL: 'postgresql://integration.invalid/sammo' }); + expect(definition.env).toHaveProperty('RELEASE_CONTROLLER_WORKSPACE_ROOT', config.workspaceRoot); expect(definition.env).not.toHaveProperty('pm_id'); expect(definition.env).not.toHaveProperty('pm_exec_path'); expect(definition.env).not.toHaveProperty('name'); diff --git a/docs/release-operations.md b/docs/release-operations.md index ab2caced..a154d422 100644 --- a/docs/release-operations.md +++ b/docs/release-operations.md @@ -114,6 +114,10 @@ release-controller는 PM2 process 안에서 실행되므로 부모의 `args`, `p Caddy upstream port는 열리지 않을 수 있습니다. 배포 readiness는 process 상태와 두 HTTP endpoint를 함께 확인해야 합니다. +Gateway process definition에는 `GATEWAY_DATABASE_URL`과 `REDIS_URL`이 모두 +필요합니다. controller는 `REDIS_URL`이 없는 환경에서는 시작 단계에서 실패하여 +불완전한 Gateway process 전환을 막습니다. + Gateway 전체에는 활성 릴리스 작업을 동시에 하나만 둘 수 있습니다. 화면의 릴리스 이력에서 요청 source, 고정 commit, 상태와 오류를 확인할 수 있습니다. @@ -154,7 +158,10 @@ pnpm --filter @sammo-ts/release-controller run-once Self-upgrade는 실행 중인 controller daemon과 다른 shell/CLI process에서 실행해 주세요. 대상 worktree를 준비하고 build와 gateway migration을 마친 뒤 -controller PM2 definition만 전환합니다. +controller PM2 definition만 전환합니다. CLI 환경에는 `GATEWAY_DATABASE_URL`과 +`REDIS_URL`을 모두 주입해야 합니다. 새 controller의 cwd는 선택 commit +worktree이지만 `RELEASE_CONTROLLER_WORKSPACE_ROOT`는 원래 Core checkout을 +유지하며, release state가 없는 최초 DEPLOY의 rollback 기준으로 사용합니다. ```sh pnpm --filter @sammo-ts/release-controller build