마이그레이션 랜딩 절차 (직렬화)
2026-09-15 개정(오너 지시, #1661·#1659) — 승격 검사 3단계 — ① 작업 PR→release는 큐 Merge Check만(PR 전 precheck 폐기) ② release→staging은 GitHub Full CI precheck 레인(
static-checks+migration-smoke) ③ staging→main은 QA2 레인(qa2-product-contracts)만. QA1은 완전히 퇴역(어느 워크플로도 실행하지 않음). 아래 2026-09-10 배너와 본문의 precheck·full CI·재사용 서술은 그 개정 전 기록이다. 정본 = 릴리스 프로세스.
2026-09-10 승인 정책 (#1491, v0.17.8 대상): 작업 세션은
ci:precheck-local, 작업→release 큐는 Merge Check만 수행한다. 최종 release→staging 후보에서 full CI를 실행하고, staging→main은 동일 tree의 유효한 full 성공 기록과 현재 staging 배포·smoke 증거를 확인해 재사용한다. 명시적merge:request --dry-run은 진단용 full 실행을 유지한다. 앱 main6afe58ec에 설치됐으며 정상 큐의 실제 실행 시간과 운영 배포 성공은 아직 확인 전이다. 상세 상태는 적용 기록에서 구분한다. 아래 과거 전환 기록보다 이 절차가 우선한다.
저장소 분리 적용 범위 (#1463): 이 문서의 실행 코드·DB·앱 release 절차는
dekerd/Barbelic에 적용한다. 문서·관리자·랜딩은 각각의 독립 저장소 절차를 따른다. 과거 docs/admin 결합·앱 main 문서 직행 설명은 분리 전 이력이며 저장소 경계와 독립 운영·실제 전환 상태가 그 부분을 대체한다.
2026-09-09 개정 (이슈 #1456·#1457·#1458, 오너 결정) — 명령·워크플로 이름이 바뀌었다. 작업 PR 통합 요청
npm run merge:request(구 landing:request), PR 전 사전 검증npm run ci:precheck-local(최신 release 합치기 → verify → 서버 변경 시 DB 단계, 브라우저 없음), 전체 검증npm run ci:full-local(릴리스 큐 runner 안에서만; 세션은--only <단계>재현만). 워크플로 표시 이름:3-Release Merge Request(release-merge-request.yml) ·GitHub Full CI(policy-contract.yml) ·1-Production Deploy/2-Staging Deploy(production-deploy.yml·staging-deploy.yml, 단계 정의는 deploy-steps.yml) ·Production smoke (manual)·Social login daily check·Daily data backup.landing:lock·db:preflight스크립트는 폐기됐다(GitHub concurrency 가 잠금, DB 검사는 사전 검증과 큐가 한다). 아래 본문의 옛 이름은 그 개정 전 기록이다.
이 문서는 migration의 번호·스냅샷·위험·데이터 보존 검증을 설명한다. release의 Merge Check·병합은 릴리스 랜딩 큐, 브랜치·CI·승격의 정본은 릴리스 프로세스다. 아래의 전환 전 main 전용 랜딩 절차는 이전 구현의 기록이다. DEPLOYMENT_BRANCH_CUTOVER=true로 새 배포를 활성화한 뒤에는 기존 main 랜딩을 실행할 수 없다. 새 경로에서도 DB 검증의 강도와 원본 보존 조건은 유지한다.
release 브랜치 작업에 적용하는 규칙
- 작업의 목적 release를 최신화하고 SQL 원천·migration·스냅샷을 준비한다. 번호는 그 release의 기존 migration 뒤에 오도록 정리하고, 승격 전에는 최신 main/staging과 실제 적용 장부도 다시 대조한다. 이미 원격에 적용한 migration은 고치거나 개명하지 않는다.
ci:precheck-local -- --release origin/release/vX.Y.Z로 최신 release 반영·정적/단위/빌드·서버 변경 시 격리 DB 검사를 수행한다. populated upgrade·원본 유실 감사 등 해당 변경의 증거를 갖춘다. 아래landing-preflight.json은 과거 구현의 기록이며 현행 큐의 새 사전검증 장부나 쓰기 권한으로 요구하지 않는다.- clean worktree와 PR head가 일치하면
npm run merge:request -- --pr <N>으로 요청한다. 큐는 최신 base와 고정 head의 후보를 만든 뒤 이력 불변성·migration 순서/위험·head/base/tree·조건부 push를 같은 슬롯에서 확인한다. 일반 요청은 full CI·DB 스택·docs/admin 빌드를 실행하거나 full 성공 기록을 만들지 않는다. 충돌·검사 실패·head/base 변경은 병합하지 않는다. - release→staging은 최신 base와 release를 합친 최종 후보의 GitHub full CI를 실행한다. staging→main은 동일 tree의 유효한 full 기록과 현재 staging 배포·smoke 증거로 재사용하며 기록이 없거나 무효이면 full을 실행한다. DB 적용은 승격 후 환경별 Deploy가 수행한다. release 통합 후 staging/Production DB에 직접 push하지 않으며 승격·배포용 전역 락을 추가하지 않는다.
도구별 기준을 구분한다. merge:request는 작업→release의 self-hosted 큐에 사용한다. 배포 전환 후 main 경로는 차단되며 두 승격은 별도 PR로 진행한다. 실제 활성화 상태는 큐 문서에서 확인한다. 아래 migrations:check·migrations:renumber와 pre-push 훅은 별도로 origin/main 기본값을 확인해야 한다. release 큐가 생겼다는 이유로 다른 도구의 비교 기준까지 바뀌었다고 가정하지 않는다. 번호는 목적 release와 승격 대상의 실제 migration 장부를 대조하며, release PR을 main으로 바꿔 기존 큐를 호출하지 않는다.
전환 전 구현의 배경
이슈 #966에서 병렬 migration의 번호 충돌과 반복 CI를 줄이려고 main 기준 번호 정리·Actions 직렬 랜딩을 도입했다. 아래 도구 표의 main/Actions 요청 설명과 main 랜딩 절차는 그 당시 구현의 기록이다. 전환 후 실행 경로는 위 release 절차와 두 승격 PR이며, 번호 순서·원격 장부·데이터 보존 검증과 HQ 상주 불필요 원칙은 유지한다.
도구
| 명령 | 하는 일 |
|---|---|
npm run migrations:check | origin/main을 받아와 번호·schema.sql 머리 지문·EXPECTED_LATEST_MIGRATION 상태를 수 초 만에 판정 (고치지 않음) |
npm run migrations:renumber | 내 묶음의 마이그레이션을 꼬리 다음 슬롯으로 개명하고, 파일 자기 주석·브랜치가 만진 파일 속 번호·schema.sql 머리의 마이그레이션 지문·EXPECTED_LATEST_MIGRATION을 함께 치환. schema.sql 본문은 건드리지 않는다(개명은 DB 상태를 바꾸지 않으므로) |
npm run schema:snapshot -- --sandbox <디렉터리> [--reset] | 로컬 샌드박스 스택에 재생된 DB의 상태를 덤프해 supabase/schema.sql을 다시 쓴다(이슈 #1252). 마이그레이션 내용이 바뀌었으면 이걸 돌려 커밋한다. --check는 비교만 하며 CI migration-smoke·ci:local·ci:precheck-local(DB 단계)가 db reset 직후 같은 검사를 한다 |
npm run ci:precheck-local -- --sandbox <디렉터리> | 로컬 샌드박스 스택에 db reset --no-seed → supabase test db(pgTAP 전 파일)를 돌리고, 통과했을 때만 이 워크트리의 .git 디렉터리에 통과 기록(landing-preflight.json: 브랜치·HEAD·supabase/migrations+supabase/tests 내용 해시·파일 수·assert 수·시각)을 남긴다. 실패하면 이전 기록도 지운다 |
npm run migrations:risk -- --write [--new] | 새 마이그레이션의 문장을 잠금·재작성·스캔·WAL·재실행·공존 부류로 분류해 -- migration-risk: 헤더를 쓴다(이슈 #1287, 정본 docs/data/populated-db-upgrade.md). check:migrations·ci:precheck-local(DB 단계)·landing:lock acquire 가 헤더가 문장과 맞는지 본다 |
npm run db:upgrade -- --from <직전 릴리스> --sandbox <디렉터리> --fixture <데이터 사본> --out <폴더> | 직전 릴리스 스키마 + 데이터가 있는 격리 DB 에 새 마이그레이션을 적용하며 시간·WAL·잠금 대기·옛 앱 프로브·중단/재실행·사실 digest·공존 diff 를 재고 -- upgrade-evidence: 줄을 낸다. level=high 마이그레이션의 랜딩 필수 증거 |
npm run db:backfill -- --spec <spec.json> --out <폴더> | 대표 fixture 실측이 예산을 넘긴 긴 백필만 배치·커서·checkpoint·재개로 돌린다(작은 변경에는 쓰지 않는다) |
npm run merge:request -- --pr <N> | 현재 clean HEAD 와 PR head 를 대조한 뒤 release 통합 큐(3-Release Merge Request)에 요청서를 낸다. 큐 runner 가 최신 release 와 합친 후보에서 번호·위험 헤더·ci:full-local·docs/admin 빌드를 검사하고 그 커밋을 그대로 release 에 반영한다(구 landing:request) |
npm run ci:precheck-local | PR 전 사전 검증 — 최신 release 합치기 → verify → 서버 파일 변경 시 DB 단계(재생·스냅샷·pgTAP). 구 db:preflight 를 흡수. landing:lock 은 2026-09-09 폐기(GitHub concurrency 가 잠금) |
node scripts/migrations/install-landing-hooks.mjs | pre-push 훅 설치 (PC당 1회, 전 워크트리 공유) — 꼬리 이하 번호의 푸시를 기계적으로 거부 |
npm run ci:wait -- --pr <N> | CI 대기 표준 절차 — 상한 35분·30초 간격 완료 조회·5분 간격 상세 보고. PR 충돌로 체크가 안 생기는 경우·이벤트 유실·후속 push의 런 취소·gh 명령 실패를 전부 판정해 조용한 무한대기를 막는다 |
실제 랜딩 슬롯은 GitHub Actions가 소유한다. 위 표의 기존 main 경로는 저장소 공통 concurrency 그룹, 새 release 경로는 목적 release별 그룹을 사용한다(queue: max, cancel-in-progress: false). 기존 ~/.barbelic/landing-lock/은 이 PC 안에서만 보이는 준비용 파일이다. 다른 PC를 잠그거나 원격 머지를 승인하는 장치로 쓰지 않는다.
랜딩 절차
이 절의 main·원격 CI·merge:request 호출은 전환 전 기존 경로의 기록이다. 배포 전환 후 그대로 실행하지 않는다. 새 작업은 위 release 절차와 릴리스 프로세스를 따른다.
0. 요청 전 준비 (자동 큐 밖에서, 시간이 걸리는 일 전부) 함수·정책·트리거·뷰·권한 변경은 supabase/definitions/<도메인>/… 원천을 고치고 npm run sql:candidate -- --slug <이름> 으로 후보 마이그레이션을 만든다(2026-09-07, 이슈 #1283, 정본 docs/data/sql-definitions.md §6). 표·열·인덱스·제약·데이터 변경은 생성기가 추측하지 않으므로 그 SQL 을 파일로 써서 --ddl <파일> 로 넘긴다. 재생·스냅샷 뒤 npm run sql:extract(registry 갱신) → npm run sql:check 가 원천 == 스냅샷 을 확인한다(npm test 도 같은 단언). 최신 main을 브랜치에 반영하고 — 리베이스로 다른 마이그레이션이 들어왔으면 npm run sql:extract 로 원천을 main 의 스냅샷에 맞춘다(Docker 불필요, 수 초) — 로컬 게이트(npm run ci:local)를 통과시켜 둔다. 리베이스에서 supabase/schema.sql·deploymentManifest.ts 충돌이 나면 아무 쪽이나 받고 넘어가도 된다 — deploymentManifest.ts는 migrations:renumber가 마이그레이션 폴더에서 다시 계산해 덮어쓰고, schema.sql은 npm run schema:snapshot -- --sandbox <디렉터리> --reset(재생한 DB의 상태 덤프)으로 다시 만든다. 손으로 병합하지 않는다.
0-1. 요청 전 preflight — 서버 테스트를 이 PC에서 먼저 통과시킨다 (2026-09-04, 이슈 #1224) CI는 확인용이지 첫 실행이 아니다. #1200·#1202 두 트랙이 pgTAP·e2e를 로컬에서 돌리지 않고 PR을 열어 CI 8회 중 7회가 로컬에서 잡을 수 있던 문제로 빨간불이 났다. 새 마이그레이션의 자동 랜딩 요청도 기존 "로컬 pgTAP 통과 기록"을 요구한다.
- 레포 밖 샌드박스를 준비한다(PC당 한 번, 이후 워크트리마다 junction만 다시 건다). 예:
<scratchpad>/sbx/supabase/에config.toml— 다른project_id와 다른 포트(예: api 55821 / db 55822 / shadow 55823),[storage] enabled = true,[analytics] enabled = false,[studio]·[inbucket]비활성.migrations,tests— 이 워크트리의supabase/migrations·supabase/tests를 가리키는 junction (PowerShellNew-Item -ItemType Junction -Path <sbx>\supabase\migrations -Target <워크트리>\supabase\migrations). 다른 워크트리를 가리키면 preflight가 거부한다 — 기록은 실제로 테스트한 파일을 설명해야 하기 때문이다..temp/postgres-version—package.json의supabaseToolchain.postgresVersion(= Production의 Postgres 이미지 태그)을 그대로 쓴 파일.supabase start가 이 태그의 이미지를 띄우므로 로컬·CI·Production이 같은 빌드에서 돈다(이슈 #1233).npm run ci:local은 이 파일을 알아서 쓴다.- 스택 기동:
cd <sbx> && supabase start -x studio,inbucket,imgproxy,logflare,vector,supavisor,realtime,edge-runtime(첫 기동 수 분, 이후 유지). 레포의supabase/config.toml은 한 번도 건드리지 않으므로npm run check의 project_id 단언과 충돌하지 않는다.
npm run ci:precheck-local -- --sandbox <sbx>— 리셋 후 pgTAP 전 파일을 돌린다.npm run ci:local(full)을 이미 통과했으면 이 단계는 끝난 것이다 — ci:local이 pgTAP 통과 시 같은 기록을 남긴다(docs/process/ci-local.md, 이슈 #1225). 이하는 preflight만 따로 돌릴 때의 설명이다(이 PC 실측: 100파일·1,601 assert, 테스트 자체 15초·리셋 포함 수 분). 통과하면 마지막 줄에 PR 본문에 붙일 "로컬 pgTAP N파일·M assert 통과" 문장이 나온다.BARBELIC_DB_SANDBOX환경변수로--sandbox기본값을 둘 수 있다.- 마이그레이션·pgTAP 파일을 한 글자라도 더 고쳤으면 다시 돌린다 — 기록이 내용 해시와 묶여 있어 고친 뒤의 랜딩 요청은 거부된다.
실행 코드·DB가 바뀐 PR은 e2e/required-coverage.json의 자동 browser 57개와 viewport 14개를 필수로 검증한다. npm run ci:local -- --full --sandbox <sbx>는 같은 로컬 Auth/DB/Edge와 앱·관리자 번들을 준비하고 완료 증거까지 집계한다. 집중 재현 결과만으로 전체 검증을 대신하지 않으며, 실제 provider QA인 CASE003은 별도 조건부 레인이다. 자세한 범위와 운영은 E2E PR 게이트 구현 기록을 따른다.
0-1-1. 위험 헤더와 populated upgrade 증거 (2026-09-07, 이슈 #1287 D13, 정본 docs/data/populated-db-upgrade.md) 새 마이그레이션마다 npm run migrations:risk -- --write 로 -- migration-risk: 헤더를 쓴다(후보를 sql:candidate 로 만든 뒤에도). 헤더의 level 이 high(populated 표의 전체 UPDATE·인덱스·제약·재작성·열/표/함수 삭제·이름 변경)면 npm run db:upgrade -- --from <직전 릴리스 ref> --sandbox <sbx> --fixture <매일 데이터 사본 또는 --workload> --out <폴더> 를 돌려 마지막 줄 -- upgrade-evidence: v=2 env=supabase-sandbox … facts=preserved verdict=passed 를 파일 머리에 붙인다. ci:precheck-local(DB 단계) 가 헤더·증거를 검사해 통과 기록에 남기고, landing:lock acquire 가 잠금 직전에 한 번 더 본다. low·medium 은 헤더만 있으면 된다(실측은 PR 본문에 권장). 실측이 예산을 넘긴 백필은 db:backfill 배치로 옮긴다.
0-2. 유실 감사 — 유저 기록 원본을 만지는 마이그레이션은 Production 실데이터로 먼저 세어 본다 (2026-09-04, 이슈 #1236, 계약 정본 docs/contracts/user-fact-immutability.md) 원칙은 "마이그레이션은 유저 원본을 바꾸지 않는다"(R5)다. 스키마·함수·투영 컬럼만 바꾸는 번들은 이 단계가 없다. 원본 등급 테이블(supabase/contracts/user-fact-columns.json)에 UPDATE/DELETE/컬럼 삭제/타입 변경이 한 줄이라도 있으면:
npm run db:loss-audit -- <마이그레이션 파일…> --out <리포트.json>— Production에서 되돌리는 트랜잭션 안에 마이그레이션을 실제로 돌려, 기본값이 아닌 원본 값이 바뀌거나 행이 지워지는 건수를 센다(수백 ms). 아무것도 남지 않는다.- 파괴·삭제가 0이 아니면 그대로 랜딩할 수 없다. 이슈에 건수·표본을 올려 오너 승인을 받고, 마이그레이션 첫 줄에
select set_config('lift_guild.repair_ticket', 'issue#NNNN', true);를 선언한다(옛 값은 보호 트리거가 이력 테이블에 자동 보관한다). - 마이그레이션 파일 안에
-- loss-audit: 파괴 N건·삭제 N행 (Production <시각>, 되돌림)줄을 적고 같은 줄을 PR 본문 "검증"에 붙인다.npm run check의check:migrations가 티켓 선언과 이 줄이 없는 원본 DML을 거부한다. - 러너 권한(42501)에 걸려 감사가 못 도는 마이그레이션은 매일 데이터 사본(
Daily data backupartifact)을 로컬 샌드박스에 복원해 같은 감사를 돈다.
1. 번호 확정과 최종 검증: 최신 main을 반영한 뒤 npm run migrations:renumber → diff 검토 → 커밋. 개명만으로 DB 상태는 바뀌지 않으므로 schema.sql은 머리 지문만 바뀐다. 다만 preflight 기록은 파일 내용 해시와 연결되므로 번호 정리 후 기록이 낡았으면 다시 실행한다. 본문 재생성은 마이그레이션 내용이 바뀐 0단계에서 끝내야 한다.
2. 푸시 → PR → CI 확인: pre-push 훅이 번호를 한 번 더 검문한다. npm run ci:wait -- --pr <N>으로 원격 검증을 확인하고 PR 본문에 검증 결과·호환 영향을 남긴다. 로컬 준비 잠금을 사용했다면 작업이 끝난 시점에 반납한다. 원격 CI 대기에 --landing-heartbeat나 HQ 라벨은 필요하지 않다.
3. 자동 랜딩 요청: npm run merge:request -- --pr <N>. 현재 worktree가 clean이고 local HEAD가 요청 PR head와 같아야 한다. 새 마이그레이션의 preflight 기록이 없거나 브랜치·파일 해시가 다르거나 24시간이 지났으면 요청이 거부된다. 출력된 명령으로 준비를 갱신한 뒤 다시 요청한다.
4. Actions의 검증 → squash merge → staging 확인: main의 workflow가 저장소 공통 큐에서 한 요청씩 실행한다. 이전 staging 진행, 요청 head가 최신 main을 포함하는지, 번호·스냅샷 검증을 포함한 GitHub Full CI의 실제 verify 성공과 전체 완료, 관리자 호환 조건을 확인한다. 대기 중 기준이 바뀌면 머지하지 않고 실패한다. 통과하면 확인한 head를 squash merge하고 deploy-steps.yml(1-Production Deploy·2-Staging Deploy 가 호출) staging을 명시적으로 호출해 merge SHA가 포함된 배포의 성공을 확인한다. 요청 후 담당 세션이 종료돼도 Actions는 계속 진행한다.
5. 결과 기록: 담당자가 PR·merge SHA·staging run URL과 필요한 앱 확인 결과를 이슈에 남긴다. staging 초록과 실제 확인 조건이 충족돼야 Staging verified다. main 머지는 staging까지만 적용된다. Production DB에 직접 db push하지 않는다. Production은 main → production 릴리스 PR의 오너 승인·merge commit 이후 기존 배포 레인이 처리한다.
중단·실패·재요청
- workflow가 실패·취소·시간 초과로 종료되면 Actions 슬롯이 반납된다. 세션이나 HQ가 잠금을 회수할 필요가 없다. 원인을 고치는 동안 큐를 점유하지 않는다.
- 머지 전 head/main이 바뀌면 담당자가 최신화하고 번호·관련 검증을 갱신한 뒤
merge:request를 다시 실행한다. 자동 큐는 PR 코드를 임의 rebase하거나 고치지 않는다. - 이미 머지된 PR도 같은 PR head로 재요청할 수 있다. 새 머지를 만들지 않고 해당 merge SHA의 staging 확인·재개를 수행한다.
- 앞선 staging이 실패했으면 무관한 다음 PR을 위에 쌓지 않는다. 실패 원인을 고치는 forward-fix PR만
npm run merge:request -- --pr <N> --repair-staging-run <실패 run ID>로 수리 대상을 명시한다. workflow는 실제deploy-steps.yml(1-Production Deploy·2-Staging Deploy 가 호출)·main·실패 결과와 현재 main의 관계를 확인한다. 이 옵션은 실패 무시나 HQ 승인 대체물이 아니라 수리 근거다.
보장 범위
landing:active·landing:queued는 과거 절차의 표시다. 있어도 실행권이 없고 없어도 요청할 수 있다. 라벨을 분산 잠금으로 사용하지 않는다.- 로컬 잠금·pre-push 훅의 우회 옵션은 자동 랜딩의 preflight·번호·required CI 검사를 우회하지 않는다. 실패한 검사를 관리자 권한으로 건너뛰지 않는다.
- Actions 큐는 이 경로로 요청한 랜딩을 직렬화한다. 오너의 웹 머지·직접 배포까지 물리적으로 잠그지는 않으므로 큐 대상 PR은 이 표준 경로를 사용한다. 자동 작업은 머지 직전 기준을 다시 확인한다.
- CI(
check:migrations)는 "새 번호는 기존 꼬리보다 커야 한다"를MIGRATION_BASE_REF기준으로 추가 검증한다. 이미 적용한 migration은 수정하지 않는다.