Skip to content

QA2 승격 게이트

2026-09-15 사용자 지시 "일단 QA2를 메인 CI로 승격시켜서, release->stage, stage->main에 적용시켜줘. 관리자 권한으로 바로 작용." 앱 작업은 #1604에서 추적한다. QA2 저장소는 비공개 dekerd/Barbelic-QA2이며, QA2의 설계·운영 원칙은 그 저장소의 README.md·docs/operating-model.md가 정본이다. 이 문서는 앱 승격 CI에서 QA2가 어떻게 실행되고, 어떻게 버전을 고정하며, 빨간불일 때 무엇을 보는지를 적는다.

무엇이 돌아가나

2026-09-15 개정(오너 지시, #1661·#1659) — QA2는 staging → main 릴리스 PR의 유일한 검사 레인이다(GitHub Full CI의 qa2-product-contracts job, 필수 검사 verify가 그 성공을 요구). release/vX.Y.Z → staging 승격 PR은 precheck 레인(static-checks + migration-smoke)만 돌고 QA2는 돌지 않는다. 같은 Git tree 의 full 성공 기록을 재사용하던 규칙은 폐기했다(main 병합 tree = 배포된 staging tree 와 staging 배포 3잡 성공은 scope 잡이 확인). 수동 실행(workflow_dispatch)은 두 레인을 모두 돌린다. 아래 첫 문단은 도입 당시(#1604) 기록이다.

앱의 GitHub Full CI(.github/workflows/policy-contract.yml)에 qa2-product-contracts job이 추가되었고, 필수 검사 verify가 그 job의 성공을 요구한다. 도입 당시(#1604)에는 release/vX.Y.Z → staging 승격 PR과 staging → main 릴리스 PR 모두 QA2를 통과해야 병합할 수 있었다.

QA1 lane(정적 검사·단위 테스트·마이그레이션·브라우저 여정·화면 정합성·증거 대조)은 2026-09-15 오너 지시로 완전히 퇴역했다(어느 워크플로도 QA1을 준비·실행하지 않음, QA1 저장소 문서). QA2가 유일한 제품 검증이며, QA1의 케이스·helper·장부는 QA2로 옮기지 않는다.

job의 단계는 다음과 같다.

  1. 앱 후보 커밋 checkout, Node는 앱 .nvmrc(22).
  2. .github/actions/prepare-qa2: 앱 qa2.lock.json의 QA2 커밋을 읽기 전용 deploy key(QA2_DEPLOY_KEY)로 .git/qa2/source에 체크아웃하고 npm ci. QA2는 앱 tree 밖에 머물며 앱 checkout에 파일을 준비하지 않는다.
  3. QA2의 Playwright Chromium 설치(버전별 캐시).
  4. QA2 자체 검사 npm run check(독립성 경계·문법·실행기 회귀 테스트).
  5. npm run qa:ci -- --app $GITHUB_WORKSPACE --revision $GITHUB_SHA --out $RUNNER_TEMP/qa2-product-ci.
  6. 결과 artifact qa2-product-contracts: runs/<run-id>/report.json·report.html·시나리오별 영수증, product-target.json. 제품 빌드 산출물은 올리지 않는다.

qa:ci(QA2 scripts/product-ci.mjs)는 한 명령으로 다음을 순서대로 수행한다. 소유 PostgreSQL 컨테이너에 후보 커밋의 마이그레이션 전체 적용 → GoTrue·PostgREST 컨테이너를 전용 docker 네트워크 qa2-net에 기동(DB에는 컨테이너 이름으로 접속) → 후보 커밋의 제품 소스만으로 웹 빌드 → 정적 서빙 → 전체 목록을 release 모드로 실행 → 컨테이너·네트워크 정리. 최종 보고서의 releaseEligible이 참일 때만 종료 코드 0을 반환한다. 부분 실행이나 focused 실행은 승격 판정에 쓰지 않는다.

버전 고정과 결과 식별

  • qa2.lock.json: schemaVersion: 1, repository: "dekerd/Barbelic-QA2", 40자리 commit. 이동하는 브랜치 이름으로 검사를 고르지 않는다.
  • QA2 변경을 승격 검사에 채택하려면 QA2에 커밋을 게시한 뒤 앱에서 이 파일의 커밋을 올린다(QA1의 qa1.lock.json과 같은 방식). 고정한 커밋은 QA2의 브랜치나 태그에서 도달 가능해야 한다. squash 병합 뒤 브랜치를 지우면 고정 커밋을 받을 수 없어 승격 CI가 준비 단계에서 실패한다.
  • 제품 커밋은 항상 호출자가 지정한다(CI에서는 GITHUB_SHA). QA2 소스에는 제품 버전 상수가 없다.
  • 검증 결과는 앱 커밋·tree(고정 파일이 tree에 들어 있어 QA2 버전까지 식별) + 실행 환경(Node, 컨테이너 이미지)으로 식별한다. QA2 버전이 바뀌면 이전 조합의 성공을 새 조합의 성공으로 쓰지 않는다.

로컬에서 같은 검사 돌리기

sh
git clone git@github.com:dekerd/Barbelic-QA2.git
cd Barbelic-QA2 && git checkout < qa2.lock.json의 commit>
npm ci && npx playwright install chromium
npm run qa:ci -- --app "<앱 checkout 절대경로>" --revision <검증할>

Docker Desktop이 켜져 있어야 하고, qa2-barbelic-db·qa2-web-auth·qa2-web-rest 컨테이너와 qa2-net 네트워크, 포트 59320~59323이 비어 있어야 한다. 실행이 끝나면 정리까지 자동으로 한다. 산출물은 QA2 work/product-ci/<시각>-<커밋>/runs/<run-id>/에 남는다. Windows에서는 PowerShell·cmd·Git Bash 어디서 실행해도 된다(압축 해제는 System32의 tar를 명시).

빨간불일 때 보는 순서

  1. artifact의 report.json에서 result.casesstatus: "failed"인 시나리오와 error를 본다. 제품 단언 위반이면 제품 결함이다. 작업 브랜치에서 고쳐 같은 3단계(Phase → Merge Check → release 병합)로 넣고 승격 후보를 다시 만든다(승격 ① precheck 레인 → staging 배포 → 승격 ② QA2 레인).
  2. 제품의 합의된 변경 때문에 QA2의 기대값이 낡은 것이면 QA2 scenarios/에서 고치고 앱의 고정 커밋을 올린다. 단언 완화·자동 재시도·skip 추가로 통과시키지 않는다.
  3. unverified(미검증)는 대상 부재·시간 초과·정리 실패다. 이미지 pull, docker 네트워크, Chromium 설치 같은 환경 문제를 먼저 본다.
  4. Full CI 실패 수리 절차가 그대로 적용된다. 실패 전체를 모아 로컬 qa:ci로 재현·수리한 뒤 승격 후보를 갱신한다. 수정 하나마다 Full CI를 반복하지 않는다.

2026-09-15 보강 — 커버리지 분모·새 의무·revision 교정 (QA2#2)

오너 지시 "QA1의 레거시를 절대 QA2까지 끌고 오지 않는다"에 따라 QA1의 케이스·helper·장부·실행기는 옮기지 않고, QA2의 단위(약속·의무·필수 입력·대상 지문·교정)로만 보강했다. 상세 설계는 QA2 저장소의 docs/operating-model.md(커버리지 절)·docs/database.md(새 시나리오·교정 절)·docs/browser.md가 정본이다.

  • 보고서에 "공개 경계 커버리지" 절이 생겼다. 실행 시작 때 검증 대상 DB에서 로그인 사용자가 부를 수 있는 RPC 문, 앱 계정에 권한이 있는 표, 설치된 예약 작업(cron)의 이름을 열거해 지문과 함께 기록하고, 의무가 선언한 경계(surfaces)와 대조해 의무 있음 / 범위 밖(이유) / 미배정을 보여 준다. 미배정은 통과가 아니라 아직 약속이 없는 경계이며 출시 판정에 들어가지 않는다. 의무가 제품에 없는 경계를 선언하면 판정 근거 오류로 그 실행은 미검증이 된다. 앱 main 157bf12b 기준 공개 경계 208개(RPC 140·표 57·예약 작업 11) 중 의무 있음 11·범위 밖 25·미배정 172.
  • 새 의무 4개(시나리오 43개로). 계정 삭제 뒤 참조 표 전수 잔여 0(db-account-deletion-residue), 티켓 없는 시스템 경로의 원본 변경 거부·이력·티켓 기록(db-system-write-protection, 원본 불변 계약 §2·§3), 서버에 닿지 못한 운동 저장의 기기 보관과 재시작 뒤 정확히 한 번 반영(browser-workout-disconnected-recovery), 모든 브라우저 의무의 "예상치 못한 오류 0"(no-unexpected-errors).
  • 진단·교정용 부분 실행. npm run qa:ci -- … --mode focused --risk <이름> --layer database는 선언한 위험의 DB 의무만 돌리고 출시 승인을 내지 않는다. 결함이 있던 제품 커밋과 고친 커밋에 같은 의무를 돌려 전자 실패·후자 통과를 남기는 방식으로, 296·1000종목 통계 의무가 v0.18.0 d2fba06a에서 제품 오류 53200(BUG-144)으로 실패하고 main에서 통과함을 QA2 evidence/2026-09-15/에 보존했다.
  • 빨간불일 때 보는 순서는 위와 같고, 보고서의 result.coverage.unknownClaims가 비어 있지 않으면 제품 결함이 아니라 낡은 카탈로그(제품에서 사라진 경계를 의무가 선언)이므로 QA2 catalog.mjssurfaces·outOfScope·retired를 고치고 앱의 고정 커밋을 올린다.

2026-09-15 보강 2차 — 미배정 공개 경계 172개 배정 (QA2#4)

1차에서 생긴 커버리지 분모 위에서, 미배정으로 남아 있던 공개 경계 172개(RPC 118·표 47·예약 작업 7)를 모두 의무에 배정하거나 사유와 함께 범위 밖으로 선언했다. QA1 장치는 옮기지 않고 계약(friend-access·app-role-privileges·user-fact-immutability)과 기록된 버그의 의미만 QA2 단위로 표현했다. 상세는 작업 기록과 QA2 저장소 docs/verification.md(QA2#4 절)·catalog.mjs(의무-경계 대응 정본)가 정본이다.

  • 미배정 0. 앱 main 157bf12b 기준 공개 경계 208개(RPC 140·표 57·예약 작업 11) 중 배정 163·범위 밖 45·미배정 0. 무결성 오류·알 수 없는 배정·낡은 범위밖 0.
  • 의무 38→55개, 시나리오 54→71개. 삭제·격리, 계획·그룹, 소셜, 조회 정합성(저장 사실의 독립 산술과 대조), 프로필·카탈로그·기록·초안·사진 문, 통계 파이프라인·예약 유지보수를 더했다.
  • 제품 결함을 잡아 releaseEligible false. 유일한 실패 의무 db-account-deletion-social-references가 "차단당한 계정 삭제 불가"(Barbelic#1624)를 잡는다. 제품을 초록으로 만들려 QA를 완화하지 않았다. #1624이 고쳐지기 전까지 release→staging 승격은 이 판정에 막힌다(의도된 동작).
  • 하네스 한계 하나(같은 날 후속으로 해소). 통계 워커 문·검증기 일부의 session_user 기반 회원 거부는 QA2 배우 연결이 DB 소유자 로그인이라 재현하지 못해 단언하지 않았다(거짓 통과 금지). auth.uid() 기반 자기 스코프·재계산·특권 워커 동작은 충실히 검증했다. → 아래 후속 절.

2026-09-15 후속 — #1624 수리 확인과 배우 로그인 분리 (QA2#4 이어서)

  • #1624 수리 확인. 수리가 든 앱 release/v0.19.3 4c4a847e에 대해 QA2 096eb5a 전체 실행: 71개 중 70 통과·실패 0·미검증 1. 차단당한 계정 삭제 의무 db-account-deletion-social-references는 통과. 미검증 1건은 db-wide-1000(1000개 종목 저장→통계 게시)이 60초 벽시계 한도를 넘긴 것으로, 같은 시각의 로컬 부하가 원인이었다(부하 없이 집중 실행하면 54.4초 통과; 이전 앱 대비 약 8% 느리지만 예산 안). 제품 결함 아님.
  • 배우 연결을 비특권 로그인으로. QA2 PR #6: 제공 스크립트가 이미지의 authenticator 역할(실제 앱 클라이언트가 접속하는 로그인)에 비밀번호를 주고, 어댑터가 배우 작업을 그 로그인의 두 번째 연결에서 실행한다. 첫 사용 전에 로그인이 superuser·BYPASSRLS·워커 로그인이 아닌지 검사해 아니면 미검증으로 거부한다(거짓 통과 방지). 회원 거부 검사 5개 복원 — 워커 일괄 문 2개(enqueue_stale_user_exercise_stats_refresh_jobs·process_user_exercise_stats_refresh_jobs), 검증기 3개의 남의 계정 거부 — 모두 42501. 의무·시나리오 수는 그대로(55·71), 필수 검사만 늘었다.
  • 결과. QA2 main 05332518 · 앱 4c4a847e 전체 실행 71/71 통과(releaseEligible true, 미배정 0/208, 시나리오 실행 2분 46초, 2026-09-15 05:58Z) · 앱 qa2.lock.json05332518: 앱 PR #1635 큐 병합 79026980.

현재 상태 (2026-09-15)

항목확인된 상태
QA2 커밋(2차 후속) QA2 main 05332518 (2026-09-15, PR #6 병합, 배우 로그인 분리·회원 거부 검사 5개) — 앱 qa2.lock.json이 이 커밋을 고정. (2차) QA2 main 096eb5a (2026-09-15, PR #5 병합, 미배정 0) — 앱 qa2.lock.json이 이 커밋을 고정. (1차) QA2 main 3bfd296b (2026-09-15, PR #3 병합) — 앱 qa2.lock.json이 이 커밋을 고정(아래 앱 반영). 직전 고정 커밋 046ae55b(2026-09-15 01:28 KST)
앱 반영(2차 후속) qa2.lock.json → QA2 05332518: 앱 PR #1635이 큐로 release/v0.19.3에 병합(병합 커밋 79026980, 큐 실행 34935322147). main·staging·Production 미반영(승격 대기). #1624 수리(4c4a847e)도 같은 release에 있어 QA2 판정이 더는 막지 않는다. (2차) qa2.lock.json → QA2 096eb5a: 앱 PR #1626이 큐로 release/v0.19.3에 병합(병합 커밋 eee3aac1, 큐 실행 34925324872). main·staging·Production 미반영(승격 대기, #1624로 막힘). (1차) qa2.lock.json → QA2 3bfd296b: 앱 PR #1623이 release 큐로 release/v0.19.3에 병합(병합 커밋 8a4b8b6b, 큐 실행 34882818805). main·staging·Production에는 아직 없다(승격 대기). (09-15 새벽) 앱 main 5d3d6ac1046ae55b를 고정(관리자 권한 직접 push; 1-Production Deploy run 34868927889 성공). QA1 pin은 a13fa64a
로컬 검증(2차 후속) QA2 자체 검사 103/103 · QA2 qa:ci로 앱 4c4a847e 전체 71/71 통과(releaseEligible true, 미배정 0/208, 시나리오 실행 2분 46초, 2026-09-15 05:58Z) · 앱 precheck 통과(static·unit-1 2079·unit-2 1809, 2분 14초) · 큐 Merge Check 통과. (2차) QA2 자체 검사 101/101 · QA2 qa:ci로 앱 main 157bf12b 전체 71개 중 70 통과(releaseEligible false — 유일한 실패는 제품 결함 #1624; 공개 경계 미배정 0/208) · 앱 precheck 통과(static·unit-1 2107·unit-2 1778) · 큐 Merge Check 통과. (1차) QA2 자체 검사 101/101 · QA2 qa:ci로 앱 main 157bf12b 전체 43/43 통과(releaseEligible true, 시나리오 실행 2분 6초 + 준비·빌드, Windows·Docker Desktop·Node 24.16.0) · 교정 4회 · (09-15 새벽) QA2 자체 검사 96/96 · 앱 커밋 5d3d6ac1 전체 40/40 통과 · 앱 precheck 통과 · 앱 워크플로 계약 테스트 163/163 · QA1 자체 검사 17/17
GitHub 첫 실행미실행. 원격 Full CI 지시가 없어 다음 승격 PR(release→staging)에서 처음 실행된다. 046ae55b·3bfd296b 모두 GitHub runner에서는 아직 돌지 않았다