릴리스 승격 검사 3단계·QA1 퇴역 — 잔여 정리와 지침 정렬 (2026-09-15)
- 기간: 2026-09-15 20:20 ~ 23:30 KST (세션 1, Claude
0272c6d3). 오너 지시 원문: "release에 병합할때는 precheck 없이 merge check만하고, release->stage 올릴때 병합된 release 기준으로 precheck만 한번, 그리고 stage->main 올릴때 QA2를 진행하는걸로 바꿔줄래" + "QA1는 이번 배포부터는 완전히 deprecated 처리 할 것 (워크플로우에서 자동실행되지 않도록 할것)". - 랜딩: 앱 이슈 #1661. 앱의 정책 반영은 같은 지시를 받은 #1659 세션의 PR #1663(
70b96140)·PR #1664(4552cb45, release/v0.19.3)로 먼저 들어갔다. 이 트랙의 PR #1665는 같은 내용이라 닫았다. 잔여 앱 정리 = PR #1677(feat/1661-queue-cleanup, release/v0.19.4 병합 커밋5d9095c5, 아래 §4). 문서 = 이 저장소 PR(본 문서 포함). 전역 지침 = 오너 CLAUDE.md §31. 마이그레이션·엣지·Vercel 배포 없음. - 설계서: 없음 — 이슈 본문의 분석·Phase 계획·예상 효과 표가 설계다.
- 정본: 릴리스 프로세스(2026-09-15 개정 배너·§1~§3·실행 기준 표), QA2 승격 게이트, 공통 규칙 §5, 세션 제목. 앱 코드 =
.github/workflows/policy-contract.yml(lane 스위치)·scripts/check-promotion.mjs·scripts/migrations/release-landing-service.mjs. - 도구: 없음(스크래치 검증 스크립트는 레포 밖).
- 게이트: 승격 ① precheck 레인 첫 실측 run 34966023249(scope·static-checks·migration-smoke·verify 성공, qa2 skipped). 잔여 정리 브랜치는
npm run check:static통과·워크플로 6개 YAML 파싱·node --check. QA1 퇴역으로 pgTAP·e2e 기록 없음. - 버그리포트: 없음(오너가 승인한 실행 정책 변경).
- 계약: 작업→release·두 승격의 검사 실행 시점, QA1 호출 계약 종료.
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 앱 승격 CI 3단계화 + QA1 자동 실행 제거 | ✅ #1659 세션 PR #1663·#1664 경유(이 트랙 PR #1665는 중복으로 닫음). 잔여 정리(큐 dry-run·tree 기록·안내문·npm 항목) = PR #1677 → release/v0.19.4 병합 5d9095c5(큐 실행 34974264364) |
| Phase 2 | 문서·지침 정렬(이 저장소 + 전역 CLAUDE.md §31) | ✅ 이 PR |
1. 배경
2026-09-10 #1491 이후 승격 검사는 "작업 세션 precheck → 큐 Merge Check → release→staging 최종 후보 full CI → staging→main은 같은 tree 성공 기록 재사용"이었다. 2026-09-15 저녁 §30(precheck 없이 Merge Check만)에 이어 오너가 밤에 나머지 두 단계를 정했다: release→staging은 병합된 release 후보에서 precheck 1회, staging→main은 QA2. 그리고 QA1(2026-09-14 #1591로 분리된 기존 테스트 전체)은 이번 배포(v0.19.3)부터 완전히 폐기해 어떤 워크플로도 자동으로 돌리지 않는다.
같은 지시가 두 세션(#1659 릴리스 준비 세션, #1661 이 세션)에 각각 내려졌다. 두 세션은 서로의 작업을 모른 채 같은 파일(승격 CI·큐·배포 워크플로·package.json·check-promotion)을 고쳤고, #1659 세션의 PR이 먼저 큐를 통과했다.
2. 문제 제기
승격 CI가 "Full 하나"로 묶여 있어 단계별로 켜고 끌 수 없었다
scope 잡이 requires_full·reused_tree 두 값만 냈고 모든 검사 잡이 requires_full == 'true' 하나에 묶여 있었다. release→staging에서 정적·DB만, staging→main에서 QA2만 고를 수 없었다.
QA1이 워크플로 7곳에 물려 있었다
승격 CI(단위 2조각·브라우저 4조각·화면 정합성·증거 대조·migration-smoke의 pgTAP·DB 테스트·e2e), 큐 runner(prepare-qa1 + dry-run의 ci:full-local), 배포 뒤 CRUD smoke(staging·Production), Production smoke (manual), 소셜 로그인 매일 점검(가정 로그인 3종), 관리자 fixture(QA1 ci-local.mjs의 상수 3개), 백업 신선도(QA1의 복원 훈련 기록). 앱 정적 검사 2곳(check:deployment의 성능 프로브 대조, check:ts-scope의 tests/ 요구)도 QA1 파일이 있어야 통과했다.
같은 지시가 두 세션에 내려져 작업이 겹쳤다
이 세션의 PR #1665는 큐 Merge Check에서 #1663·#1664와 10개 파일 충돌(run 34966327943)로 실패했다. 공유 인프라를 바꾸기 전에 같은 release의 열린 PR·최근 병합을 확인하는 단계가 없었다.
3. 해결 방안
원칙 (오너 결정, 2026-09-15)
- D1 "release에 병합할 때는 precheck 없이 Merge Check만" — 채택(§30과 동일).
- D2 "release→stage는 병합된 release 기준으로 precheck만 한 번" — 채택. precheck =
static-checks+migration-smoke(빈 DB 재적용·schema.sql 대조). 단위 테스트·pgTAP는 QA1 소유라 D4로 함께 빠진다. - D3 "stage→main은 QA2" — 채택. 같은 tree 성공 기록 재사용은 폐기.
- D4 "QA1은 이번 배포부터 완전히 deprecated(워크플로 자동 실행 0)" — 채택.
접근
| 안 | 내용 | 판단 |
|---|---|---|
| scope 잡이 lane을 판정하고 잡마다 lane 조건으로 켜진다 (#1663) | lane = staging / production / manual, static-checks·migration-smoke는 staging·manual, qa2-product-contracts는 production·manual | 채택(반영됨) — 정책이 코드 한 곳에 있고 필수 검사 verify 이름·보호 규칙 불변 |
| 스위치 3개(static·db·qa2)로 잡을 켜고 DB 단계는 서버 파일 변경 시만 (#1665) | 승격 diff에 supabase/·`scripts/sql | migrations/`가 있을 때만 migration-smoke |
| 승격 ①·② 워크플로를 따로 만든다 | yml 2개 | 기각 — 필수 검사 이름·concurrency·prepare 액션이 두 벌로 갈라진다 |
| QA1을 남기고 워크플로에서만 뺀다 | qa1.lock.json·hook 유지 | 기각 — 오너가 "완전히"라고 했고 남은 hook이 앱 명령에 QA1을 끌어들인다(#1663이 전부 삭제) |
4. 적용한 내용
Phase 1 — 앱 (PR #1663·#1664, #1659 세션)
policy-contract.yml: lane 출력, QA1 잡 4종 삭제, migration-smoke를 재적용·schema.sql 대조만으로.verify는 lane별 성공/skip 대조.check-promotion.mjs: tree 재사용 삭제, staging 증거 = database·functions·frontend 잡 성공.- 배포·큐·백업·관리자 fixture에서
prepare-qa1제거,prod-smoke.yml·social-login-daily-check.yml·prepare-qa1액션·scripts/qa1.mjs·qa1.lock.json삭제, npm hook 31개·test·smoke:*삭제,check=check:static. 관리자 fixture는 앱 소유scripts/ci/local-sandbox-config.mjs로. - 첫 실측: 승격 ① run 34966023249 성공(잡 4개). 승격 ② QA2 레인은 v0.19.3 릴리스 PR에서 처음 돈다.
Phase 1 잔여 — PR #1677 (release/v0.19.4 병합 5d9095c5)
release/v0.19.3 4552cb45에 아직 남은 것: 큐 runner dry-run이 QA1 ci:full-local·legacy tag·tree 성공 기록을 부르는 경로(merge:request --dry-run은 실패한다) → Merge Check만 하고 push 안 함; merge:request 안내문·큐 워크플로 주석/입력 설명; policy-contract.yml의 tree 기록 저장 단계·full_ci 입력·checks: write; scripts/ci/validated-trees.mjs 삭제; 배포 호출자의 issues: write; 관리자 fixture 트리거 경로; 없는 QA1 파일을 가리키던 npm 항목 5개(ci:local·ci:precheck-local·ci:full-local·check:test-manifest·check:error-cases); ci-wait 안내문. v0.19.3이 이미 staging에 승격돼 정리만으로 재승격을 만들지 않기 위해 v0.19.4에 넣었다 — 오너 지시("v0.19.4 만들어서 머지해줘")로 release/v0.19.4를 main dd7d2828(v0.19.3 릴리스 PR #1671 병합 직후)에서 만들고, 최신 main을 합친 뒤 큐로 병합했다(큐 코드는 main 도달 뒤에만 효력).
Phase 2 — 문서·지침 (이 PR + 전역 CLAUDE.md)
- 이 저장소:
AGENTS.md앱 CI·릴리스 통합 절(3단계 원문·폐기 목록),agent-shared-rules.md(구현 완료 후 절·§5 배너·검증 명령 표·hosted CI·워크플로 이름 표),session-titles.md([Precheck]폐기),release-landing-queue.md,release-process.md(배너·§1·표·§3),ci-local.md(기록으로 격하),deployment-pipeline.md순서 1·2,ci-flow.md·branch-workflow.md·migration-landing.md배너,qa2-promotion-gate.md(QA2 = 승격 ② 유일 레인),docs/updates/README.md게이트 줄. - 전역 CLAUDE.md §31: 지시 원문, 세 단계, QA1 폐기, 세션 규칙 변화(
npm run check= 정적만, 단위 테스트 서술 폐기, QA2 부분 실행), 첫 실측, 교훈.
주요 결정과 그 근거
- "precheck"의 정의를
static-checks+migration-smoke로 둔 것: 단위·pgTAP는 QA1 소유라 D4와 양립할 수 없고, 마이그레이션 재적용·schema.sql 대조는 앱 소유라 남길 수 있다. - 중복 PR을 닫고 잔여만 맡은 것: §27(다른 에이전트 PR 불간섭)과 "사용자 목표 보존" — 목표는 이미 반영됐고, 충돌 해결로 같은 변경을 두 번 넣을 이유가 없다.
작업 중 드러난 것
- 같은 오너 지시가 두 세션에 내려져 같은 파일을 동시에 고쳤다. 공유 인프라(워크플로·큐·package.json)를 바꾸기 전에
gh pr list --base release/vX.Y.Z와git log --first-parent origin/release/vX.Y.Z로 같은 범위의 PR·병합을 확인해야 한다(CLAUDE.md §31 교훈). - Git Bash에서
git show ref:.github/...는 MSYS 경로 변환에 걸려 빈 출력이 된다 —MSYS_NO_PATHCONV=1을 붙여야 한다. 이 때문에 release 쪽 워크플로 상태를 처음에 잘못 읽었다. - 앱 정적 검사 2곳이 QA1 파일 존재에 기대고 있었다(
check:deployment·check:ts-scope). #1664가 같은 것을 고쳤다. - #1663이
Social login daily check를 통째로 지웠는데 그 안의 provider-config 잡(앱 스크립트)은 QA1이 아니다 → 복원 여부 오너 결정 대기.
5. 적용 결과
| 항목 | 전 → 후 |
|---|---|
| 승격 ① 잡 실행 수 | 13(정적·단위 2·DB·브라우저 4·화면·증거·QA2·verify·scope) → 4(scope·static-checks·migration-smoke·verify), 실측 run 34966023249 |
| 승격 ② 검사 | 같은 tree 기록 재사용(없으면 Full 13) → QA2 레인 1회(scope·qa2·verify) — 미실측(v0.19.3 릴리스 PR에서 처음) |
| 작업 PR 전 세션 검증 | ci:precheck-local 11분 × 재합치기 → 0(Phase마다 npm run check 정적만) |
| 워크플로에서 QA1 자동 실행 | 7곳 → 0 |
| 남은 QA1 잔재 | 큐 dry-run 경로·tree 기록 단계·안내문·npm 항목 5개 → PR #1677로 release/v0.19.4 병합(큐 runner 코드는 main 도달 뒤 효력) |
| 문서의 "precheck 필수·full CI·재사용" 서술 | 9개 문서 → 배너·본문 개정(이 PR) |
미검증: 정리된 큐 runner 코드의 실제 첫 실행(v0.19.4가 main에 도달한 뒤 새 요청부터). 승격 ② QA2 레인의 GitHub 첫 실행 결과는 v0.19.3 릴리스 PR #1671의 #1659 기록을 따른다.
6. 이번 개선으로 향상된 것
- 승격 판정이 lane 하나로 읽힌다(release→staging = 정적·마이그레이션, staging→main = 제품 계약).
- QA1 유지 비용(앱 변경마다 QA1 테스트 갱신·핀 올림)이 사라졌다.
- 지침(전역·저장소)이 실제 워크플로와 같은 말을 한다.
남은 것
- release/v0.19.4의 승격 ①·②(릴리스 담당 몫). 정리된 큐 runner는 v0.19.4가 main에 도달한 뒤 효력.
- 소셜 로그인 제공자 설정 매일 점검(provider-config) 복원 여부 — 오너 결정.
scripts/review-account/verify.mjs·scripts/migrations/release-v0171-projection-rollout.mjs가 QA1ci-local.mjs를 import한다(수동 도구·과거 롤아웃 스크립트) — 언급만, 이번 범위 밖.