Skip to content

QA2를 승격 CI 필수 검사로 적용 (2026-09-15)

  • 기간: 2026-09-14 ~ 2026-09-15 (세션 2개, 오너 지시 원문 "일단 QA2를 메인 CI로 승격시켜서, release->stage, stage->main에 적용시켜줘. 관리자 권한으로 바로 작용")
  • 랜딩: QA2 main 046ae55b · QA1 브랜치 qa1/1604-verify-qa2-lane a13fa64a · 앱 main 5d3d6ac1 (2026-09-15 01:29 KST 관리자 권한 직접 push, PR·큐 없음) — 마이그레이션·엣지 변경 없음, main push로 자동 시작된 1-Production Deploy run 34868927889 성공(01:34 KST)
  • 설계서: 앱 #1604 본문(분석·Phase 3개·예상 효과 표)
  • 정본: QA2 승격 게이트, 앱 .github/workflows/policy-contract.ymlqa2-product-contracts job과 verify 필수 조건, 앱 qa2.lock.json·.github/actions/prepare-qa2, QA2 scripts/product-ci.mjs(npm run qa:ci)
  • 도구: QA2 scripts/product-ci.mjs(DB 준비 → 인증/REST → 제품 빌드 → 서빙 → 전체 실행 → 정리 한 명령), QA2 scripts/subject-db.mjs(제품 커밋 입력·qa2-net 네트워크)
  • 게이트: 앱 GitHub Full CI에 QA2 lane 추가(verify 필수). 로컬 검증은 §5 표. 원격 Full CI는 이번 지시 범위 밖이라 미실행
  • 버그리포트: 없음
  • 계약: QA1 suite/tests/react/ciLocal.test.mjs(verify 필수 조건 5개 lane, job 경계 숫자 허용, QA2 lane 고정 검사) — 조정 사유는 QA1 migration-adaptations.json에 기록

Phase 현황

Phase내용상태
Phase 1QA2: 제품 커밋 입력화·컨테이너 네트워크·한 명령 CI 실행기·Node 22 허용·scale 시나리오를 #1590 배치 계약에 맞춤✅ QA2 main 046ae55b
Phase 2앱: qa2.lock.json·prepare-qa2·qa2-product-contracts job·verify 필수화·deploy key/secret, QA1 계약 테스트 갱신·pin✅ 앱 main 5d3d6ac1
Phase 3문서·기록✅ 이 문서

1. 배경

2026-09-14에 기존 Full CI 테스트 전체가 QA1(dekerd/Barbelic-QA1)로 이관됐고, 같은 날 제품 약속 기반의 독립 검증 시스템 QA2(dekerd/Barbelic-QA2)가 만들어졌다. QA2는 로컬에서 한 번 실행됐을 뿐 CI에 연결되지 않았다. 오너는 QA2를 승격 CI(release→staging, staging→main)에 필수 검사로 넣도록 지시했다.

2. 문제 제기

검사 대상 제품 커밋이 QA2 소스에 상수로 박혀 있었다

QA2는 검증 대상 커밋 d2fba06a를 소스 세 곳에 상수로 갖고 있어 승격 후보 커밋을 검사할 수 없었다.

QA2의 컨테이너 연결이 Docker Desktop에서만 통했다

인증(GoTrue)·REST(PostgREST) 컨테이너가 DB에 host.docker.internal로 붙었다. GitHub의 Linux runner에서는 이 이름이 없고 DB 포트도 127.0.0.1에만 열려 있어 컨테이너가 DB에 닿지 못한다.

승격 CI의 필수 조건 목록이 QA1 계약 테스트에 고정돼 있었다

verify가 요구하는 lane 목록과 그 판정 스크립트는 QA1의 ciLocal.test.mjs가 그대로 고정하고 있어, lane을 추가하면 테스트가 막는다(설계된 동작). 또 그 테스트의 job 경계 정규식이 숫자가 든 job 이름을 경계로 인식하지 못했다.

3. 해결 방안

원칙

  • 오너 지시(2026-09-15): QA2를 메인 CI로 승격해 두 승격 경로에 적용하고, 관리자 권한으로 바로 반영한다.
  • 전제(세션 판단): QA1 lane은 유지하고 QA2를 추가 필수 검사로 넣는다. QA1 제거는 별도 지시가 있을 때만.

접근

QA1과 같은 형태로 앱이 QA2 커밋을 qa2.lock.json에 고정하고 읽기 전용 deploy key로 읽는다. QA2에는 로컬과 CI가 같은 명령으로 부르는 실행 정의(npm run qa:ci)를 두고, 제품 커밋은 호출자가 지정한다. 컨테이너는 전용 docker 네트워크에서 이름으로 연결해 Docker Desktop과 Linux runner에서 같은 방식으로 동작하게 했다. 대안으로 검토한 "호스트 게이트웨이 주소 추가"는 DB 포트를 모든 인터페이스에 열어야 해 채택하지 않았다.

4. 적용한 내용

Phase 1 — QA2 저장소

  • 제품 커밋 입력화: subject-db.mjs start <repo> <revision>, subject-web-build.mjs … <revision>, subject-web-serve.mjs … <revision>; DB·웹 대상 검증이 대상 설정의 productRevision과 대조한다. 상수 세 곳 제거.
  • qa2-net 네트워크: DB 컨테이너가 네트워크에 참여하고 인증·REST 컨테이너가 컨테이너 이름으로 접속. 시작 시 생성, 정지 시 비어 있으면 제거.
  • scripts/product-ci.mjs(npm run qa:ci): 이미지 확인/pull → DB 준비 → 인증/REST 기동 → 제품 웹 빌드 → 서빙 → 전체 목록 release 모드 실행 → 정리. releaseEligible이 참일 때만 종료 코드 0. 정리 실패는 성공을 막는다.
  • Node 범위를 >=22.13.0 <25로 넓혀 앱 .nvmrc(22)와 QA1 계약("모든 setup-node는 .nvmrc")을 함께 만족.
  • scale 시나리오(296·1000 종목)를 #1590의 배치 계약에 맞춤: 동기 worker가 첫 배치를 준비하고 batch_pending으로 미루면, production cron이 매초 부르는 claim → compute → publish 진입점을 같은 statement 예산으로 반복 호출해 세대가 게시될 때까지 몰고 간다. 단언은 "실패 잡 0·게시 완료"로 유지.
  • Windows에서 압축 해제에 System32의 tar를 명시(Git Bash의 GNU tar가 C:\ 경로를 원격으로 해석하던 실행 환경 문제).
  • 자체 검사 3개 추가(인수 검사·커밋 해석·대상 revision 거부).

Phase 2 — 앱 저장소

  • qa2.lock.json, .github/actions/prepare-qa2/action.yml(pin 검증 → deploy key로 .git/qa2/source 체크아웃 → npm ci), policy-contract.ymlqa2-product-contracts job(QA2 자체 검사 → qa:ci → 결과 artifact)과 verify 필수 조건(QA2_RESULT).
  • QA2 deploy key(읽기 전용)와 앱 secret QA2_DEPLOY_KEY 등록. 비밀키 파일은 등록 직후 삭제.
  • QA1 qa1/1604-verify-qa2-lane(a13fa64a, base = 앱 main의 기존 pin f2de2849): ciLocal.test.mjs의 verify 필수 목록·판정 매트릭스에 다섯째 lane 반영, job 경계 정규식에 숫자 허용, QA2 lane 고정 검사 추가, migration-adaptations.json에 조정 기록. 앱 qa1.lock.json을 이 커밋으로 갱신.

작업 중 드러난 것

  • QA2의 첫 통합 실행이 db-wide-296·db-wide-1000에서 실패했다. 원인은 제품 결함이 아니라 #1590(2026-09-14) 이후 100종목 초과 계산이 배치로 나뉘어 동기 호출 한 번으로 끝나지 않는 새 계약이었다. QA2 시나리오를 production cron과 같은 경로로 고쳤고 단언은 완화하지 않았다.
  • QA1 계약 테스트의 job 경계 정규식 [a-z-]+e2e-evidence도 경계로 보지 못해 앞 job 본문에 뒤 job이 섞이고 있었다(기존에는 npm run 줄이 없어 드러나지 않음). [a-z0-9-]+로 고쳤다.
  • 큐·PR을 거치지 않는 관리자 직접 반영이라 원격 Full CI에서 QA2 lane이 실제로 도는 것은 다음 승격 PR에서 처음 확인된다.
  • 작업 중 앱 main이 v0.19.0 릴리스(#1606·#1608)로 앞서가며 qa1.lock.json6db676c3에서 f2de2849로 바뀌었다. 이전 세션이 6db676c3 위에 만든 QA1 변경(7ed2e892)은 그대로 고정할 수 없어 f2de2849 위로 옮겨(cherry-pick, migration-adaptations.json은 두 목록을 합쳐 해결) a13fa64a로 다시 게시했다. 같은 이유로 release/v1.0.0처럼 다른 QA1 커밋을 고정한 release 브랜치는 승격 전 main을 반영할 때 qa1.lock.json 충돌이 나며, 해결은 QA1에서 두 커밋을 합친 커밋을 만들어 다시 고정하는 기존 절차다.
  • 이전 세션이 켜 둔 QA2 4회차 실행이 세션 종료로 28/40에서 끊기며 qa2-* 컨테이너 3개와 qa2-net 네트워크, work/subject-web/private.json이 남았다. 실행기 프로세스가 강제로 죽으면 subject-web-api.mjs stop이 게이트웨이 프로세스가 없어 실패하므로 docker rm -f·docker network rm으로 손 정리해야 한다. CI의 일회용 runner에서는 해당 없다.

5. 적용 결과

항목결과
QA2 자체 검사Node 22.23.2에서 96/96(독립성 59모듈, 기존 93 + 신규 3)
QA2 제품 검증(앱 커밋 5d3d6ac1, QA2 046ae55b)전체 40/40 통과, releaseEligible true, 5분 2초(Windows·Docker Desktop·Node 22.23.2), 정리 뒤 컨테이너·네트워크 잔재 없음
QA1 자체 검사verify + runner 17/17
앱 정적 검사check:static·check:unused 통과
앱 워크플로 계약 테스트QA1 a13fa64a로 준비한 뒤 policy-contract.yml·qa1.lock.json을 참조하는 10파일 163/163 통과(ciLocal·ciScopePolicy·ciToolchain·ciWait·deploymentTargets·eslintGate·selfServiceLanding·statsRefreshStatementTimeoutContract·typescriptUnusedGate·validatedTrees)
앱 precheck검증: ci:precheck-local precheck · static 통과 · unit-1 통과 2107 passed / 0 failed / 64 conditional skip · unit-2 통과 1778 passed / 0 failed / 85 conditional skip · 2분 7초 (커밋 5d3d6ac1, origin/main 1755f24a 위)
main push 자동 실행1-Production Deploy run 34868927889 성공 — resolve·database·functions·frontend·smoke·release tag 성공, browser journeys 잡은 workflow에서 꺼져 있어(if: false) skip, 새 태그 없음(커밋 메시지에 버전 없음) · Export admin backend fixture run 34868926533 성공
원격 Full CI미실행 — 이번 지시에 원격 Full CI가 없어 QA2 lane의 GitHub 첫 실행은 다음 승격 PR에서 확인된다

6. 이번 개선으로 향상된 것

승격 판정에 제품 약속 검증이 들어간다

release→staging·staging→main 승격은 QA1과 함께 QA2의 제품 약속 6개·의무 24개·입력 40개를 통과해야 한다. QA2 버전은 앱 tree에 고정되어 어떤 QA2로 판정했는지 기록에 남는다.

QA2가 임의의 후보 커밋을 어디서나 검사한다

제품 커밋이 입력이 되었고 컨테이너 연결이 플랫폼에 의존하지 않아, 로컬(Windows)과 GitHub(Linux)에서 같은 한 명령으로 같은 검사를 한다.

남은 것

  • 다음 승격 PR에서 GitHub Full CI의 QA2 lane 첫 실행 결과 확인(이번 지시 범위 밖).
  • release/v0.19.0·v1.0.0 승격 전 main 반영 시 qa1.lock.json 충돌 해소(각 승격 세션).