# 릴리스 운영 매뉴얼

이 문서는 Core2026의 profile 서버와 Gateway를 Git commit 단위로 배포하고
되돌리는 현재 운영 경계를 설명합니다. Profile은 Gateway orchestrator가,
Gateway 전체는 별도 release-controller가 처리합니다.

## 구성과 권한

| 대상                              | 요청 경로           | 실행자                  | 영속 상태                                        |
| --------------------------------- | ------------------- | ----------------------- | ------------------------------------------------ |
| `che`, `hwe` 등 profile           | Gateway 관리자 화면 | Gateway orchestrator    | `GatewayOperation`, `GatewayProfile`             |
| Gateway API·frontend·orchestrator | Gateway 관리자 화면 | 외부 release-controller | `GatewayReleaseOperation`, `GatewayReleaseState` |
| release-controller 자체           | 별도 CLI process    | self-upgrade CLI        | PM2 `sammo:release-controller`                   |

Profile 화면은 `/gateway/admin/servers/:profileName/version`과
`/gateway/admin/servers/:profileName/scenario`, Gateway 화면은
`/gateway/admin/releases`입니다. 이전 `/gateway/admin/server-operations`는
호환성을 위해 서버 목록으로 이동합니다. Profile 작업은 runtime/settings/deploy/reset
capability로 분리되며 포괄 운영 권한은 사용하지 않습니다. Gateway 전체 릴리스에는
profile 범위 권한과 별개인 전역 `admin.releases.manage` 권한이 필요합니다.
일반 사용자와 권한이 없는 관리자는 Gateway 릴리스 영역을 사용할 수 없습니다.

운영 전에 다음을 확인해 주세요.

- 대상 branch 또는 전체 commit SHA가 Core2026 저장소에 존재합니다.
- 대상 commit의 `release-manifest.json`에 필요한 component와 현재 migration
  head가 들어 있습니다.
- Gateway PostgreSQL, profile PostgreSQL, Redis와 PM2가 준비되어 있습니다.
- controller가 `GATEWAY_DATABASE_URL`, `GATEWAY_DB_SCHEMA`, workspace와
  worktree 경로를 올바르게 읽습니다.
- 공개 hostname을 쉼표로 구분한 `VITE_PREVIEW_ALLOWED_HOSTS`가 Gateway와
  profile frontend build 환경에 전달됩니다. 신뢰된 reverse proxy 뒤가 아니라면
  `*`를 사용하지 않습니다.
- PM2 definition은 Gateway에 `GATEWAY_ROLE=api|orchestrator`, game API에
  `GAME_API_ROLE=server|*-worker`, turn daemon에
  `GAME_ENGINE_ROLE=turn-daemon`을 명시합니다. library import나 PM2 wrapper의
  argv만으로 실행 역할을 추론하지 않습니다.
- PM2 child에서 상속된 `pm_id`, `name`, `pm_exec_path`, `NODE_APP_INSTANCE`와
  `axm_*` 메타데이터는 새 definition에 전달하지 않습니다. 동일 process 이름은
  시작 전에 제거하며 PM2 start는 남은 동일 이름을 거부합니다.
- Runtime process는 10초 이전의 불안정 종료에 대해 최대 5회, 2초 간격으로만
  자동 재시작합니다. Readiness는 예상 process 수가 정확하고 모든 restart count가
  0일 때만 성공합니다.
- Root와 server package의 `tsdown`은 0.22.14 계열로 통일합니다. Docker runtime의
  Node heap/Rayon 상한을 상속한 동일 toolchain으로 초기 Gateway와 profile
  worktree를 빌드하여 구형 Rolldown의 과도한 native thread 생성을 피합니다.
- Profile, Gateway와 controller self-upgrade의 server package build는 Turbo DAG를
  사용합니다. 실행 중인 game/Gateway process와 4 GiB runtime을 공유하는 기본
  동시성은 1이며, 더 큰 격리 build host에서는 `RELEASE_TURBO_CONCURRENCY`를 측정 후
  2 이상으로 올릴 수 있습니다. 기본 local cache는 원래 Core checkout의
  `.turbo/release-cache`이므로 commit별 worktree가 달라도 재사용됩니다. 별도
  persistent 경로가 필요하면 controller/orchestrator 환경에 `TURBO_CACHE_DIR`을
  설정합니다. Cache는 재생성 가능한 build artifact이며 DB/Redis backup이 아닙니다.
- Server build 뒤 frontend release는 `typecheck:release`와 Vite bundle을 별도 Turbo
  task로 실행합니다. 타입검사 hash에는 frontend 자체와 직접 해석하는 common/infra/
  logic/game-engine/game-api/gateway-api source, Prisma schema와 기본 navigation resource가
  들어가며, bundle hash에는 `NODE_ENV`와 모든 `VITE_*`가 포함됩니다. 따라서 내부 type
  source, base path나 API URL이 다른 frontend artifact를 cache hit로 잘못 복원하지
  않습니다. 일반 개발용 `pnpm --filter <frontend> build`의 typecheck 계약은 그대로입니다.
  `NODE_OPTIONS`와 `RAYON_NUM_THREADS`는 출력에는 영향을 주지 않는 resource 제한으로
  build child에 전달됩니다.
- `TURN_DAEMON_NODE_OPTIONS`가 설정되어 있으면 Gateway orchestrator는 그 값을
  turn-daemon PM2 process의 `NODE_OPTIONS`로만 덮어씁니다. API·frontend·worker는
  공용 `NODE_OPTIONS`를 계속 사용합니다. Profile의 `vue-tsc`와 Vite build만 더 큰
  heap이 필요하면 `PROFILE_FRONTEND_BUILD_NODE_OPTIONS`를 지정합니다. 이 값은
  profile frontend release task의 `NODE_OPTIONS`만 덮어쓰며 server package Turbo
  build와 배포 후 PM2 process의 heap은 바꾸지 않습니다. 전용 heap을 늘릴 때는
  runtime container hard limit과 전체 process RSS를 먼저 확인합니다.
- migration 이후 이전 애플리케이션으로 돌아갈 때 schema 하위 호환성이
  유지됩니다.

## Commit worktree 자동 정리

Profile orchestrator와 Gateway release-controller는 서로 다른 worktree root를
사용하지만 같은 보존 정책을 적용합니다. 각 daemon은 시작 시 한 번, 이후 24시간마다
자신이 소유한 commit worktree를 점검합니다.

- `GatewayProfile.buildWorkspace`, `RUNNING`/`QUEUED` profile 빌드 대상,
  `GatewayReleaseState`의 active/previous workspace는 기간과 무관하게 보호합니다.
- 활성 PM2 process의 cwd 또는 script 아래에 있는 worktree도 보호합니다. 여기에는
  self-upgrade된 release-controller worktree도 포함됩니다.
- 보호 대상이 아닌 worktree는 마지막 prepare 이후 최소 24시간을 유예하고, 그중
  최신 2개는 재시도 cache로 더 남깁니다. 나머지는 Git worktree로 제거하고
  `git worktree prune --expire now`로 사라진 metadata를 정리합니다.
- tracked 또는 untracked 변경이 있으면 자동 삭제하지 않습니다. Git 제거 실패를
  raw directory 삭제로 우회하지 않으며 다음 주기까지 보존합니다.
- 정리는 commit checkout과 재생성 가능한 build artifact만 대상으로 합니다.
  Gateway/profile PostgreSQL, Redis, image, runtime data volume에는 접근하지 않습니다.

따라서 하루 안에 매우 많은 commit을 연속 배포하면 유예 구간만큼 일시적으로 늘 수
있지만, active/rollback/current profile 경로 외의 장기 누적은 다음 정리 주기에
제거됩니다. Profile 관리자 API의 `admin.profiles.cleanupWorkspaces`는 같은 보호
규칙을 사용하므로 진행 중인 build/operation이 있으면 전체 정리를 보류합니다.

PM2의 Node client는 한 process 안에서 공유 connection을 사용합니다. Profile
orchestrator가 시작될 때 reconcile과 worktree 정리가 동시에 `connect/list/disconnect`를
호출해 한 callback이 사라지면, PM2에는 orchestrator가 `online`으로 보이면서도 정리
flag가 풀리지 않아 profile operation poll이 계속 대기할 수 있습니다. 제품 경계에서는
PM2 session을 직렬화하고 `connect/list`를 5초, start/stop/delete를 30초로 제한합니다.
Timeout은 scheduled task 오류로 끝나 정리 flag를 해제하고 다음 5초 operation poll이
queue를 다시 claim하게 합니다. PM2 mutation이 timeout된 경우에는 같은 mutation을 즉시
직접 반복하지 않고 실제 process 목록과 operation terminal 상태를 먼저 재조회합니다.

## Profile 배포

버전 업데이트 화면에서 profile의 branch 또는 commit을 선택합니다. Branch는 worker가
작업을 claim할 때 commit으로 해석하며, commit 입력은 전체 SHA로 고정됩니다.
같은 profile에는 `QUEUED` 또는 `RUNNING` 작업을 동시에 하나만 둘 수 있습니다.
Profile 작업과 Gateway 전체 릴리스 claim도 같은 PostgreSQL advisory lock으로
직렬화합니다. 따라서 profile `RESET`/`DEPLOY`가 실행 중이면 Gateway 릴리스는
queue에서 기다리고, Gateway 릴리스가 실행 중이면 새 profile 작업이 기다립니다.
Gateway process 전환이 진행 중인 profile migration·seed 실행자를 중단하지 않는
운영 계약입니다.

상대 Gateway 릴리스가 terminal인데도 profile 작업이 두 번의 poll 주기 이상
`QUEUED`, `attempts=0`이면 단순 build 지연이 아닙니다. Gateway orchestrator의 PM2
상태뿐 아니라 최근 started 로그, 활성 release row와 profile operation row를 함께
확인합니다. DB에 활성 release가 없고 API/frontend/profile runtime이 정상인 경우에만
현재 definition의 `sammo:gateway-orchestrator` 한 process를 재시작해 queue poll을
복구할 수 있습니다. Container, release-controller와 game daemon은 함께 재시작하지
않습니다.

### DB 유지 배포

`DB 유지 배포`는 현재 시즌을 계속 운영하면서 코드를 교체할 때 사용합니다.

1. 대상 commit의 game API target과 그 transitive engine/worker artifact를 빌드한 뒤,
   profile frontend typecheck와 bundle을 공유 Turbo cache에서 복원하거나 생성합니다.
2. 기존 profile PM2 process를 정지합니다.
3. profile game schema에 `prisma migrate deploy`를 실행합니다.
4. Scenario seed를 실행하지 않고 frontend, API, daemon과 worker를 시작합니다.
5. HTTP와 모든 PM2 role의 readiness가 확인된 뒤 build commit을 게시합니다.

이 모드는 현재 scenario, status와 인게임 DB를 유지합니다. Migration이
데이터를 변환할 수 있으므로 대상 migration의 운영 데이터 영향은 배포 전에
별도로 검토해 주세요.

Profile frontend bundle은 package의 `.release-build`에 고정 생성되어 profile base
path별 Turbo cache에 저장됩니다. Orchestrator는 cache 복원 후 이를
`.release-dist/<profileName>/game-frontend`에 staging directory를 거쳐 교체합니다.
따라서 같은 commit·같은 공개 prefix의 재배포는 `vue-tsc`와 Vite를 다시 실행하지
않고, 여러 instance가 같은 prefix를 쓰더라도 각 runtime target은 따로 materialize됩니다.
`RESET`은 선택 worktree의 Gateway profile-seed CLI도 실행하므로 기존처럼
`gateway-api`까지 server build에 포함하고, seed를 호출하지 않는 `DEPLOY`만 명시적
server target을 `game-api`로 제한합니다.

Profile process 전환 중에는 frontend/API port가 잠시 닫힐 수 있습니다. 이때 이미
열린 Gateway 로비의 profile 상세 조회가 실패하면 로비는 10초 request timeout과
1·2·3·5·8·15초(이후 15초 상한) 재시도로 자동 복구를 시도합니다. 상세가 아직
없으면 `서버 응답을 기다리고 있습니다.`와 `지금 다시 확인`을
표시합니다. 정상 응답을 한 번 받은 profile은 실패한 지도·인증 재확인 중에도
마지막 상세를 유지합니다.

### 시나리오 초기화

시나리오 초기화는 새 시즌이나 새 scenario로 현 시즌 데이터를 교체할 때
사용합니다. 기본 `서버 지정 버전`은 마지막으로 성공한 상위 권한자의 배포 소스를
따릅니다. Branch가 지정되어 있으면 worker가 작업을 claim할 때 원격 최신 commit을
해석하고, commit이 지정되어 있으면 그 full SHA를 계속 사용합니다. 따라서 Git 입력과
`admin.profiles.deploy` 권한 없이도 일반 초기화 권한자가 branch 업데이트를 포함해
초기화할 수 있습니다. 새 branch
또는 commit을 함께 배포하는 모드는 `admin.scenarios.reset`과
`admin.profiles.deploy`를 모두 요구합니다. Source와 scenario를 확인한 뒤 turn 간격, 가오픈·정식 오픈,
NPC와 자동 진행 설정을 확인하고 요청해 주세요.

같은 commit이 이미 active profile workspace에 설치되어 있으면 build command는 생략하고
migration과 scenario seed부터 진행합니다. 상위 권한자가 버전 업데이트에서 commit을
선택한 경우에만 이후 일반 초기화가 그 commit에 고정됩니다. 다시 branch를 선택해
성공적으로 배포하면 branch 추적으로 돌아갑니다.

턴 간격과 고급 옵션은 서버 상태 화면에서 저장한
`GatewayProfile.meta.resetDefaults`를 최초값으로 사용합니다. 서버별 기본값을
바꾸려면 `admin.profiles.settings:<name>` 권한으로 메타를 저장하고 시나리오
초기화 화면을 다시 여세요. 값이 없거나 유효하지 않으면 60분, 동기화·가상
장수·연장·토너먼트 사용, 기본 NPC, 이미지 단계 3, 전체 가입과 유저 자동턴
미사용이라는 기존 시스템 기본값으로 돌아갑니다. 실행마다 달라지는 scenario와
예약·가오픈·정식 오픈 시각은 자동 입력하지 않습니다.

이 모드는 build와 migration 후 기존 season/tick metadata를 읽고 scenario seeder를 실행합니다.
빈 profile schema도 migration을 먼저 적용하므로 최초 `world_state` 조회가
table 부재로 실패하지 않습니다. 현 시즌의 장수,
국가, 도시, command queue와 시장·경매 등은 새 scenario 기준으로 교체됩니다.
다음 장기보존 자료는 reset 범위 밖에 있으므로 기수를 넘어 유지됩니다.

- `hall`, `ng_games` 명예의 전당과 게임 이력
- 연감과 과거 장수·국가 기록
- 왕조·상속 자료
- legacy storage와 진단·오류 기록

초기화 작업은 되돌릴 수 있는 앱 rollback과 다릅니다. 운영 DB backup과 새
scenario 설정을 확인한 뒤 실행해 주세요.

### 잘못 연 게임 취소

게임 취소는 새 시즌을 만드는 `RESET`도, 코드를 되돌리는 release rollback도
아닙니다. `/gateway/admin/servers/:profileName/cancel`에서 별도
`admin.games.cancel:<name>` 권한으로만 요청합니다. 대상은 `PREOPEN`, `RUNNING`,
`PAUSED` 상태여야 하며 이력 보존/삭제, 플레이 장수 기록 보존/삭제, 게임 중 획득한
유산 포인트의 보전율(기본 0%)과 5자 이상의 사유를 확정해야 합니다.

Orchestrator는 Gateway에 게시된 현재 commit을 고정하고 해당 commit의 build와
game migration을 준비합니다. 그 뒤 profile을 `STOPPED`로 fence하고 process를
내린 다음 게임 schema advisory lock과 단일 transaction 안에서 다음을 수행합니다.

- 오픈 원금에 관리자가 고른 획득분 보전율을 적용해 참가자별 유산 포인트 정산
- `ng_games`를 `ABANDONED`로 남기거나 삭제하고, 원래 기수·시나리오·사유·정산표는
  `game_cancellation` 감사 행에 보존
- 선택에 따라 현재/사망 장수의 과거 기록을 `OldGeneral`로 보존하거나 모두 삭제
- 명예의 전당, 과거 국가, 황제, 연감, 통일 finalization·상속 결과처럼 정식 완료를
  뜻하는 파생 자료 삭제
- `world_state`를 취소 표시하고 profile을 재개 불가능한 `CANCELLED`로 전환

Transaction 전 실패하면 원래 profile 상태와 process 복구를 시도합니다. 정산
transaction이 commit된 뒤에는 실패한 작업을 재개해 게임을 다시 열지 않습니다.
동일 operation ID와 server ID는 중복 정산되지 않으며, 실패·재시도 기록은 Gateway
operation log와 `game_cancellation`에 남습니다. 취소 후에는 새 시나리오 초기화로만
profile을 다시 사용하세요.

### Profile 실패와 재시도

Build는 현재 runtime을 멈추기 전에 수행합니다. Migration 또는 새 process
readiness가 실패하면 작업은 `FAILED`가 됩니다. DB 유지 배포는 이전 worktree의
process 복구를 시도합니다. 관리자 화면의 오류와 PM2 process 상태를 확인한
뒤 원인을 해결하고 실패한 작업을 재시도해 주세요. 재시도는 처음 고정된 commit을
사용합니다.

Turn daemon의 DB persistence interactive transaction은 기본 30초입니다. 정상 turn
budget과 같은 Prisma 기본 5초를 그대로 쓰면 populated season의 flush가 경계에서
rollback되고 profile이 `PAUSED`로 전환될 수 있습니다. timeout을 늘려도 한
transaction의 계산·RNG·write 순서는 바뀌지 않지만 lock 보유 상한도 함께 늘어나므로
운영에서는 실제 flush 시간과 DB 경합을 함께 관찰합니다.

로비 한 행만 위 안내에 머물 때는 먼저 새 배포를 요청하지 말고 다음 순서로
구분합니다.

1. 로비의 `지금 다시 확인` 또는 browser 새로고침으로 같은 profile을 다시
   조회합니다.
2. `lobby.profiles`의 상태와 runtime role을 함께 봅니다. `RUNNING`, `PREOPEN`,
   `PAUSED`, `COMPLETED`는 API process가 유지되는 상태이므로 로비 상세와 공개 지도를
   조회합니다. `PAUSED`에는 `턴 진행 일시정지`를 함께 표시하며, 상세 조회 실패나
   폐쇄 상태로 바꾸어 표시하지 않습니다.
3. 상태가 `RUNNING`/`PREOPEN`/`COMPLETED`이고 공개
   `/<profile>/api/trpc/lobby.info`가 이미 200이면 runtime은 복구된 것이므로
   `DB 유지 배포`, `중지`, `재개`를 누르지 않습니다. 기존 로비의 전환 중 1회
   실패였을 가능성이 큽니다.
4. 상태가 `PAUSED`이면 turn daemon이 오류 또는 관리자 요청 때문에 턴 진행 gate를
   닫은 것입니다. 활성 DEPLOY/RESET이 없고 `lastError`의 원인이 일시적이거나 이미
   해소됐음을 확인한 뒤 `재개`를 한 번 사용합니다. runtime role이 모두 RUNNING이어도
   profile status가 자동으로 `RUNNING`으로 돌아가지는 않습니다.
5. 응답이 계속 502/connection refused이면 관리자 작업 이력에서 활성 DEPLOY/RESET과
   terminal 오류, 해당 profile의 API/daemon/worker runtime 상태를 확인합니다. 활성
   작업이 있으면 중복 작업을 만들지 말고 readiness 또는 rollback 종료를 기다립니다.
6. profile이 `STOPPED`이고 활성 작업이 없을 때만 `재개`를 사용합니다. metadata는
   RUNNING인데 process가 계속 없으면 자동 reconcile과 operation 오류를 먼저 확인하고,
   원인이 없는 상태에서만 마지막 복구 수단으로 `중지` 후 `재개`를 사용합니다. DB
   보존 배포는 health restart 버튼이 아닙니다.

## Gateway 전체 배포

Gateway는 자기 process를 직접 교체하지 않습니다. 관리자 화면에서 `Gateway
배포`를 요청하면 외부 `sammo:release-controller`가 다음 순서로 처리합니다.

1. Source ref를 commit SHA로 고정하고 commit worktree를 준비합니다.
2. Release manifest의 protocol, component와 migration head를 검증합니다.
3. Gateway API server build를 공유 Turbo cache에서 준비하고, frontend는 별도 cached
   typecheck와 Vite bundle task로 한 번씩 실행한 뒤 gateway migration을 적용합니다.
4. `sammo:gateway-api`, `sammo:gateway-frontend`,
   `sammo:gateway-orchestrator`를 새 worktree definition으로 전환합니다.
5. Gateway API `/healthz`, `/gateway/`와 세 PM2 process의 `online` 상태를
   확인합니다.
6. 모두 준비된 경우에만 현재·이전 commit과 workspace를 게시합니다.

Gateway migration은 앱 rollback 때 자동으로 역방향 적용되지 않습니다. 따라서
profile identity 분리의 첫 단계는 기존 `profile_name`과 legacy `scenario`를 유지한
채 `instance_key`와 nullable `current_scenario`를 추가합니다. DB trigger가 구버전의
`scenario` write와 신버전의 `current_scenario` write를 양방향 동기화하므로 migration
적용 뒤 readiness가 실패해 직전 Gateway worktree로 돌아가도 기존 DB와 시즌을
그대로 사용할 수 있습니다.

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 process definition에는 `GATEWAY_DATABASE_URL`과 `REDIS_URL`이 모두
필요합니다. controller는 `REDIS_URL`이 없는 환경에서는 시작 단계에서 실패하여
불완전한 Gateway process 전환을 막습니다.

Gateway 전체에는 활성 릴리스 작업을 동시에 하나만 둘 수 있습니다. 화면의
릴리스 이력에서 요청 source, 고정 commit, 상태와 오류를 확인할 수 있습니다.
작업을 선택하면 관리자 화면이 `admin.releases.logs`를 최대 20초씩 long polling하여
commit 해석, worktree 준비, build 명령 출력, migration, process 전환,
readiness와 rollback 진행을 커서 순서대로 이어 붙입니다. 완료된 작업의 로그도
같은 이력에서 다시 열 수 있으며 화면은 최근 1,000줄을 유지합니다.
Build 로그의 `cache hit`/`cache miss`와 마지막 `Cached: N cached, M total`은 실제
이번 릴리스의 cache 사용 여부를 나타냅니다.

로그 원본은 Gateway DB의 `GatewayReleaseLog`에 작업별로 저장되고 작업 삭제 시
함께 제거됩니다. Controller는 ANSI 제어 문자를 제거하고 secret·token·password
계열 환경 변수 값과 URL password를 저장 전에 가립니다. 최초로 이 migration을
적용하는 배포에서는 log table이 생기기 전의 build 구간을 기록할 수 없지만,
migration 이후 단계와 다음 릴리스부터는 전체 진행 로그를 기록합니다. 로그가
관리자 전용이라고 해도 credential이나 실제 환경 파일 내용을 명령 출력에
의도적으로 남기지 마세요.

### Gateway rollback

`이전 Gateway로 rollback`은 `GatewayReleaseState`에 기록된 바로 이전 commit을
다시 배포합니다. 새 Gateway의 readiness가 실패하면 controller는 이전 세
process definition을 복구합니다.

Prisma migration은 역방향으로 적용하지 않습니다. 따라서 rollback 대상 앱이
이미 적용된 새 gateway schema를 읽을 수 있어야 합니다. Schema 호환성이
확인되지 않은 릴리스는 GUI rollback에 의존하지 말고 backup·restore를 포함한
별도 복구 절차를 준비해 주세요.

## Release-controller CLI

CLI는 repository 루트에서 Git에 포함되지 않은 안전한 환경 변수 또는 secret
주입 상태로 실행합니다. 실제 database URL이나 credential을 명령행, 로그 또는
문서에 남기지 말아 주세요.

현재 릴리스 상태와 최근 작업을 확인합니다.

```sh
pnpm --filter @sammo-ts/release-controller status
```

대기 중인 Gateway 작업을 한 건만 처리하고 종료합니다.

```sh
pnpm --filter @sammo-ts/release-controller run-once
```

운영 daemon은 `app/release-controller/dist/index.js daemon`을
`sammo:release-controller` PM2 process로 실행합니다. 상세한 최초 설치와 환경
변수는 저장소의 `app/release-controller/README.md`를 확인해 주세요.

### Controller self-upgrade

Self-upgrade는 실행 중인 controller daemon과 다른 shell/CLI process에서
실행해 주세요. 대상 worktree를 준비하고 build와 gateway migration을 마친 뒤
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
pnpm --filter @sammo-ts/release-controller self-upgrade BRANCH main
# 또는
pnpm --filter @sammo-ts/release-controller self-upgrade COMMIT <full-sha>
```

새 controller가 제한 시간 안에 `online`이 되지 않으면 이전 definition을
복구합니다. Self-upgrade 중에도 migration downgrade는 수행하지 않습니다.

`release-manifest.json`의 `controllerProtocol`이 현재 controller가 지원하는
값보다 높으면 일반 Gateway 배포는 시작 전에 실패합니다. Protocol 2부터
release-controller가 `GatewayReleaseLog` 진행 로그를 저장하는 것이 계약입니다.
로그 기능이 포함된 Gateway API/frontend만 먼저 배포하면 화면은 polling하지만
구형 controller는 로그를 만들 수 있으므로, protocol 변경 commit은 위
`self-upgrade`로 controller를 먼저 전환한 뒤 Gateway 배포를 요청해야 합니다.
명시적인 self-upgrade 경로만 다음 controller protocol의 manifest를 읽을 수 있고,
schema head·component 검사는 그대로 수행합니다.

## 운영 확인 목록

배포 전:

- Source commit과 `release-manifest.json`을 확인합니다.
- DB backup, migration 영향과 rollback schema 호환성을 확인합니다.
- 대상 profile과 Gateway의 현재 commit·workspace를 기록합니다.
- controller, PostgreSQL, Redis와 PM2 상태를 확인합니다.

배포 후:

- 작업이 `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와 핵심 게임 상태가 유지됐는지
  확인합니다.
- 시나리오 초기화에서는 새 시즌 상태와 명예의 전당·연감 등 장기보존 자료를
  함께 확인합니다.
- 게임 취소에서는 profile이 `CANCELLED`이고 process가 정지됐는지, 취소 이력과
  장수 기록 옵션, 참가자별 원금·획득·보전·최종 포인트가 요청과 같은지 확인합니다.

Local unit, 격리 DB integration과 fixture Chromium 통과는 운영 PM2, 외부
Caddy/HTTPS, 방화벽과 실제 운영 DB 전환을 증명하지 않습니다. 운영 배포에서는
위 확인 목록을 실제 서비스 경로에서 다시 수행해 주세요.
