fix: preserve Gateway release runtime context

This commit is contained in:
2026-08-08 15:57:38 +00:00
parent f4273709ae
commit 6a333cdf05
6 changed files with 57 additions and 5 deletions
+7
View File
@@ -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를 함께
+7 -1
View File
@@ -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(),
},
};
};
@@ -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
);
+1 -1
View File
@@ -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,
},
});
@@ -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');
+8 -1
View File
@@ -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