From 5e82fb52c6ebbc378b7eb931ba090f8e5b23f360 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 8 Aug 2026 15:05:55 +0000 Subject: [PATCH] fix: isolate child PM2 arguments --- .../src/orchestrator/processManager.ts | 1 + app/gateway-api/test/orchestratorPlan.test.ts | 3 +++ .../test/pm2ProcessManager.test.ts | 19 +++++++++++++++++++ app/release-controller/README.md | 5 +++++ .../test/releaseController.test.ts | 8 +++++--- docs/release-operations.md | 10 ++++++++++ 6 files changed, 43 insertions(+), 3 deletions(-) diff --git a/app/gateway-api/src/orchestrator/processManager.ts b/app/gateway-api/src/orchestrator/processManager.ts index ba24364f..942f82cc 100644 --- a/app/gateway-api/src/orchestrator/processManager.ts +++ b/app/gateway-api/src/orchestrator/processManager.ts @@ -24,6 +24,7 @@ export interface ProcessManager { const PM2_INTERNAL_ENV_KEYS = new Set([ 'NODE_APP_INSTANCE', + 'args', 'autorestart', 'autostart', 'created_at', diff --git a/app/gateway-api/test/orchestratorPlan.test.ts b/app/gateway-api/test/orchestratorPlan.test.ts index e146fd67..4d739886 100644 --- a/app/gateway-api/test/orchestratorPlan.test.ts +++ b/app/gateway-api/test/orchestratorPlan.test.ts @@ -179,6 +179,7 @@ describe('buildProcessDefinitions', () => { DATABASE_URL: 'postgresql://integration.invalid/sammo', VITE_APP_BASE_PATH: '/gateway', GATEWAY_ROLE: 'orchestrator', + args: 'daemon', NODE_APP_INSTANCE: '2', name: 'sammo:gateway-orchestrator', pm_id: '2', @@ -189,6 +190,7 @@ describe('buildProcessDefinitions', () => { for (const definition of Object.values(definitions)) { expect(definition.env).toMatchObject({ DATABASE_URL: 'postgresql://integration.invalid/sammo' }); expect(definition.env).not.toHaveProperty('pm_id'); + expect(definition.env).not.toHaveProperty('args'); expect(definition.env).not.toHaveProperty('pm_exec_path'); expect(definition.env).not.toHaveProperty('name'); expect(definition.env).not.toHaveProperty('NODE_APP_INSTANCE'); @@ -206,6 +208,7 @@ describe('sanitizeManagedProcessEnv', () => { PATH: '/usr/local/bin:/usr/bin', GATEWAY_ROLE: 'orchestrator', GAME_API_ROLE: 'server', + args: 'daemon', NODE_APP_INSTANCE: '2', name: 'sammo:gateway-orchestrator', pm_id: '2', diff --git a/app/gateway-api/test/pm2ProcessManager.test.ts b/app/gateway-api/test/pm2ProcessManager.test.ts index 7d270c54..b6074766 100644 --- a/app/gateway-api/test/pm2ProcessManager.test.ts +++ b/app/gateway-api/test/pm2ProcessManager.test.ts @@ -11,6 +11,7 @@ describe('buildPm2StartOptions', () => { env: { DATABASE_URL: 'postgresql://integration.invalid/sammo', GAME_API_ROLE: 'server', + args: 'daemon', pm_id: '2', pm_exec_path: '/srv/sammo/app/gateway-api/dist/index.js', name: 'sammo:gateway-orchestrator', @@ -31,9 +32,27 @@ describe('buildPm2StartOptions', () => { }, }); expect(options.env).not.toHaveProperty('pm_id'); + expect(options.env).not.toHaveProperty('args'); expect(options.env).not.toHaveProperty('pm_exec_path'); expect(options.env).not.toHaveProperty('name'); expect(options.env).not.toHaveProperty('NODE_APP_INSTANCE'); expect(options.env).toHaveProperty('GAME_API_ROLE', 'server'); }); + + it('keeps explicit child arguments when a PM2 parent exposes its own args in the environment', () => { + const options = buildPm2StartOptions({ + name: 'sammo:gateway-frontend', + script: '/srv/sammo/app/gateway-frontend/node_modules/vite/bin/vite.js', + cwd: '/srv/sammo/app/gateway-frontend', + args: ['preview', '--host', '0.0.0.0', '--port', '15000'], + env: { + args: 'daemon', + name: 'sammo:release-controller', + pm_id: '3', + }, + }); + + expect(options.args).toEqual(['preview', '--host', '0.0.0.0', '--port', '15000']); + expect(options.env).not.toHaveProperty('args'); + }); }); diff --git a/app/release-controller/README.md b/app/release-controller/README.md index d01039d1..c70b4155 100644 --- a/app/release-controller/README.md +++ b/app/release-controller/README.md @@ -13,6 +13,11 @@ 5. 두 HTTP endpoint와 세 PM2 process가 모두 준비된 경우에만 현재·이전 릴리스 상태를 게시합니다. 실패하면 이전 세 프로세스를 복구합니다. +Controller는 PM2 자식으로 실행되지만 자신의 `args=daemon`과 PM2 identity를 +Gateway process 환경에 전달하지 않습니다. 이 값이 frontend 정의를 덮으면 Vite가 +의도한 preview port 대신 기본 개발 port로 실행될 수 있으므로, process `online` +여부뿐 아니라 Gateway API와 frontend HTTP readiness를 모두 확인합니다. + ## 환경 변수 - `GATEWAY_DATABASE_URL`: Gateway PostgreSQL URL입니다. 필수입니다. diff --git a/app/release-controller/test/releaseController.test.ts b/app/release-controller/test/releaseController.test.ts index a87f9a77..78c65858 100644 --- a/app/release-controller/test/releaseController.test.ts +++ b/app/release-controller/test/releaseController.test.ts @@ -119,9 +119,7 @@ const gatewayNames = ['sammo:gateway-api', 'sammo:gateway-frontend', 'sammo:gate it('runs Gateway preview from the frontend workspace dependency', () => { const definitions = buildGatewayProcessDefinitions('/srv/sammo/release', config); const frontend = definitions.find((definition) => definition.name === 'sammo:gateway-frontend'); - expect(frontend?.script).toBe( - '/srv/sammo/release/app/gateway-frontend/node_modules/vite/bin/vite.js' - ); + expect(frontend?.script).toBe('/srv/sammo/release/app/gateway-frontend/node_modules/vite/bin/vite.js'); }); it('does not forward release-controller PM2 identity to Gateway processes', () => { @@ -130,6 +128,7 @@ it('does not forward release-controller PM2 identity to Gateway processes', () = baseEnv: { DATABASE_URL: 'postgresql://integration.invalid/sammo', GATEWAY_ROLE: 'orchestrator', + args: 'daemon', name: 'sammo:release-controller', pm_id: '3', pm_exec_path: '/srv/release-controller.js', @@ -139,6 +138,7 @@ it('does not forward release-controller PM2 identity to Gateway processes', () = for (const definition of definitions) { expect(definition.env).toMatchObject({ DATABASE_URL: 'postgresql://integration.invalid/sammo' }); expect(definition.env).not.toHaveProperty('pm_id'); + expect(definition.env).not.toHaveProperty('args'); expect(definition.env).not.toHaveProperty('pm_exec_path'); expect(definition.env).not.toHaveProperty('name'); } @@ -250,6 +250,7 @@ describe('resolveReleaseControllerConfig', () => { const resolved = resolveReleaseControllerConfig({ GATEWAY_DATABASE_URL: 'postgresql://user:pass@127.0.0.1:5432/sammo', RELEASE_CONTROLLER_WORKSPACE_ROOT: '/srv/sammo/controller', + args: 'daemon', pm_id: '3', name: 'sammo:release-controller', axm_monitor: '{}', @@ -260,6 +261,7 @@ describe('resolveReleaseControllerConfig', () => { RELEASE_CONTROLLER_WORKSPACE_ROOT: '/srv/sammo/controller', }); expect(resolved.baseEnv).not.toHaveProperty('pm_id'); + expect(resolved.baseEnv).not.toHaveProperty('args'); expect(resolved.baseEnv).not.toHaveProperty('name'); expect(resolved.baseEnv).not.toHaveProperty('axm_monitor'); }); diff --git a/docs/release-operations.md b/docs/release-operations.md index 80c0e504..ea474f3e 100644 --- a/docs/release-operations.md +++ b/docs/release-operations.md @@ -107,6 +107,13 @@ Gateway는 자기 process를 직접 교체하지 않습니다. 관리자 화면 확인합니다. 6. 모두 준비된 경우에만 현재·이전 commit과 workspace를 게시합니다. +release-controller는 PM2 process 안에서 실행되므로 부모의 `args`, `pm_id`, +`pm_exec_path`, `name`, `NODE_APP_INSTANCE`와 `axm_*` 같은 PM2 내부 값을 자식 +환경으로 전달하지 않습니다. 특히 부모의 `args=daemon`이 frontend의 +`vite preview --host 0.0.0.0 --port 15000` 인자를 덮으면 PM2 상태만 `online`이고 +Caddy upstream port는 열리지 않을 수 있습니다. 배포 readiness는 process 상태와 +두 HTTP endpoint를 함께 확인해야 합니다. + Gateway 전체에는 활성 릴리스 작업을 동시에 하나만 둘 수 있습니다. 화면의 릴리스 이력에서 요청 source, 고정 commit, 상태와 오류를 확인할 수 있습니다. @@ -173,6 +180,9 @@ pnpm --filter @sammo-ts/release-controller self-upgrade -- COMMIT - 작업이 `SUCCEEDED`이고 고정 commit이 요청한 commit과 같은지 확인합니다. - PM2 process 이름별 항목이 정확히 하나이고 restart count가 0이며, cwd와 script가 게시된 worktree를 가리키는지 확인합니다. +- Gateway frontend의 실제 인자가 `preview --host 0.0.0.0 --port 15000`을 + 유지하고 container 내부 `127.0.0.1:15000/gateway/`와 Caddy에서 + `runtime:15000/gateway/`가 모두 응답하는지 확인합니다. - `/gateway/` 또는 대상 profile prefix에 직접 접속하고 새로고침합니다. - API health, tRPC, SSE와 정적 자산 경로를 확인합니다. - DB 유지 배포에서는 현재 season/scenario와 핵심 게임 상태가 유지됐는지