CI 브라우저 저니 병렬화 — 한 잡 11분에서 네 잡 동시 실행으로 (2026-09-04)
- 기간: 2026-09-04 (세션 1개
b59a92ee, #1233 직후. 오너 질문 "CI가 오래 걸리는 게 설치 때문 아닌가?" → 실측 답변 → "잡을 여러 개 띄울 수 있는지 확인해줘" → "응 병렬화 해줘") - 랜딩: PR #1240(Phase 1~3, squash 머지) — 마이그레이션·엣지 없음, Vercel 배포 없음(워크플로·테스트·문서)
- 설계서: 이슈 #1239 본문("예상 효과·개선사항" 절 포함)
- 정본:
.github/workflows/policy-contract.yml(잡 구조),.github/actions/local-supabase-stack/action.yml(로컬 스택 준비 — 도구 버전·CLI·이미지 핀·기동·이미지 검사) - 도구: 없음(CI 구조 변경).
npm run ci:local은 같은 단계를 직렬로 돈다 - 게이트:
tests/react/ciLocal.test.mjs(세 잡의npm run순서 합집합 = ci:local 순서, 각 잡이 공용 action을 쓰는지, 샤드·fail-fast·증거 이름),statsRefreshStatementTimeoutContract.test.mjs(이미지 검사 단계는 action 파일에서 대조),ciScopePolicy.test.mjs(새 잡의 수동/라벨 이름 가드). CI 1회 - 버그리포트: 없음
- 계약:
docs/process/ci-local.md"CI와 어떻게 같은가" 표,docs/process/branch-workflow.mdcheck context 목록
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 공통 준비를 composite action local-supabase-stack으로 | ✅ PR #1240 |
| Phase 2 | 잡 분리 — migration-smoke(DB 핵심) / browser-journeys 2샤드 / viewport-matrix | ✅ PR #1240 |
| Phase 3 | 문서·작업 기록 | ✅ PR #1240 |
| Phase 4 | npm ci를 lockfile 해시 캐시 action으로(첫 실측의 registry 지연 차단) — 오너 "그거까지 넣어서 진행, CI 하지 말고 빨리" | ✅ PR #1242 |
1. 배경
CI(Repository checks)의 full 레인은 migration-smoke 한 잡이 11분을 차지했다. 오너가 "설치 시간을 미리 로컬 Docker에 만들어 두면 줄지 않나"를 물었고, 단계별 실측(run 33830161945)으로 답했다: 도구 설치 25초·스택 기동 2분 16초·마이그레이션 재적용 1분 5초·pgTAP 15초·node e2e 9초·브라우저 저니 5분 48초·뷰포트 1분 26초. 설치가 아니라 저니 7분이 문제였고, 저니는 잡을 나눠 동시에 돌릴 수 있었다.
2. 문제 제기
나눌 수 있는 일이 한 잡에 직렬로 묶여 있었다
- 저니 31건은
provisionThrowawayUser로 자기 계정을 만들고 지우므로 서로 독립이고, Playwright에--shard=k/n이 내장돼 있다. 그런데workers: 1로 한 잡에서 한 건씩 돌았다. - GitHub Actions는 한 워크플로의 잡을 동시에 돌린다(개인 계정 상한 20). verify·migration-smoke가 이미 동시에 돌고 있었다.
준비 단계가 워크플로에 풀려 있어 잡을 늘리면 그대로 복제될 판이었다
도구 버전 읽기 → CLI → 이미지 핀 → 기동 → 이미지 검사(#1233)가 migration-smoke 본문에 있었다. 잡을 넷으로 나누면 네 번 복제되고, 핀 규칙을 고칠 때 네 곳을 고쳐야 한다.
3. 해결 방안
원칙 (오너 결정)
- D1 (2026-09-04): "응 병렬화 해줘" — 벽시계 11분 → 약 6.5분을 위해 Actions 사용량 약 1.8배를 받아들인다(비공개 저장소, 요금에 걸림을 사전 고지).
접근
| 대안 | 판단 |
|---|---|
| 잡 분리 + Playwright 샤드 + 공용 composite action (채택) | 준비 1곳, 샤드 수는 matrix 한 줄, 저니 실패가 다른 결과를 가리지 않음 |
한 잡 안에서 workers 늘리기 | 저니들이 한 로컬 DB·한 앱 서버(4173)를 공유 — 계정은 독립이어도 앱 서버·브라우저 자원이 겹쳐 플레이키 위험. 기각 |
| Supabase 이미지를 GitHub 캐시에 저장 | 1~2GB 캐시 복원이 pull과 비슷한 시간. 기각 |
| 마이그레이션을 미리 적용한 이미지 | 35초 절약에 "빈 DB에서 전부 재적용" 검증이 사라짐. 기각 |
4. 적용한 내용
Phase 1 — 공용 준비 action (PR #1240)
.github/actions/local-supabase-stack/action.yml:package.jsonsupabaseToolchain읽기 →supabase/setup-cli→supabase/.temp/postgres-version쓰기 →supabase start -x …→ 이미지 태그 검사. migration-smoke에 있던 단계를 글자 그대로 옮겼다(composite 단계라shell: bash명시만 추가). 호출 쪽은 checkout·setup-node를 먼저 한다.
Phase 2 — 잡 분리 (PR #1240)
migration-smoke(이름·watchdog 유지): 준비 →db reset --local --no-seed→supabase test db(5분) → 실패 진단 → node e2e 3개.browser-journeys:strategy.matrix.shard: [1, 2],fail-fast: false, 잡별 concurrency 그룹(샤드 번호 포함),timeout-minutes: 20. 준비 → npm ci → Chromium 캐시 →npm run test:e2e-browser -- --shard=k/2→ 증거 업로드(…-shard-k).viewport-matrix: 준비 → npm ci → Chromium 캐시 →npm run test:e2e-viewport→ 스크린샷 업로드.timeout-minutes: 15.- 세 잡 모두
needs: scope·if: requires_full, 수동 실행/무관 라벨 이름 가드(manual-*,ignore-unrelated-label-*)를 기존 잡과 같은 식으로. 브라우저·뷰포트 잡은db reset을 생략한다 —supabase start가 마이그레이션을 전부 적용하므로 같은 스키마이고, "빈 DB 재적용" 검증은 migration-smoke가 계속 한다. - 테스트:
ciLocal.test.mjs는 세 잡 본문의npm run순서 합집합이ci:local의 단계 순서와 같은지, 각 잡이 공용 action을 쓰는지, 워크플로 본문에run: supabase start·setup-cli·핀 쓰기가 다시 나타나지 않는지(두 곳 어긋남 방지)를 본다.statsRefresh…의 이미지 검사 앵커는 action 파일로.ciScopePolicy.test.mjs에 새 잡의 이름 가드 단언.
Phase 3 — 문서 (PR #1240)
docs/process/ci-local.md대응 표를 네 줄(준비 action·migration-smoke·browser-journeys·viewport-matrix)로,docs/process/branch-workflow.md의 check context 목록에 새 잡 추가, 이 기록.
Phase 4 — 설치 캐시 (PR #1242)
.github/actions/npm-install/action.yml:node_modules를runner.os·node 22·package-lock.json해시로 캐시(actions/cache)하고 캐시가 없을 때만npm ci. 네 잡(verify·migration-smoke·browser-journeys·viewport-matrix)의- run: npm ci를 이 action으로 교체.- 근거: 첫 실측에서
npm ci가 네 잡 모두 3분(평소 3초)이라 벽시계가 12분 32초로 목표에 못 미쳤다 — 원인은 패키지 서버 지연(우리 결함 아님). 캐시가 있으면 lockfile이 바뀌지 않는 한 패키지 서버에 가지 않는다. 잡마다 스택을 띄우는 2분은 병렬 구조의 고정 비용으로 남는다. - 테스트:
e1rmPolicy.test.mjs의- run:목록 기대값에서 npm ci 제거 + action 사용 4회 단언(감사 신고 갱신),ciLocal.test.mjs에 세 잡이 캐시 action을 쓰는지 단언. - CI 생략(오너 지시) — 근거: 워크플로만의 변경, 단위 테스트 4파일 통과. 첫 캐시 채우기와 벽시계 실측은 다음 full 실행에서.
주요 결정과 그 근거
- 샤드 2개: 저니 5분 48초를 반으로 → 가장 긴 잡 ≈ 기동 2분 16초 + 설치 40초 + 3분 ≈ 6분. 3개면 5분대지만 잡 하나가 더 늘어 사용량이 2배를 넘는다. matrix 한 줄이라 뒤에 바꾸기 쉽다.
- 브라우저 잡에서
db reset생략: 1분 5초 절약. 기동이 곧 전체 적용이라 검증 손실이 없다. fail-fast: false: 한 샤드가 죽어도 다른 샤드·뷰포트의 증거가 남는다 — 실패 원인 추적이 한 번에 끝난다.- main에 required check가 없다(
branches/main/protection404, 규칙은 production 브랜치에만). 새 잡 이름을 보호 규칙에 등록할 것은 없었다. 머지는 종전대로 세션이--admin으로 한다.
작업 중 드러난 것
- composite action의
run단계는shell:이 필수라 워크플로에서 옮길 때 한 줄씩 붙는다 — 기존 테스트의name → run인접 정규식이 깨져shell: bash를 허용하도록 고쳤다. - 워크플로 주석에 "
supabase startalready applies…"를 쓰자 "본문에 기동이 다시 나타나면 실패" 단언이 주석에 걸렸다 — 단언을run: supabase start·uses: setup-cli·.temp/postgres-version형태로 좁혔다.
5. 적용 결과
| 항목 | 결과 |
|---|---|
| full 레인 벽시계 시간 | 약 11분(run 33830161945: migration-smoke 659초) → 이번 실행(run 33832020178) 12분 32초 — 목표 미달. 네 잡 전부에서 npm ci가 3분(평소 3초, 같은 캐시 히트 · registry/러너 지연으로 verify도 44초)이라 가장 긴 잡 browser-journeys(2)가 728초. npm ci를 평소 값으로 놓으면 그 잡은 125+3+13+216 ≈ 6분 7초 — 구조적으로는 목표(약 6분)에 맞는다. 다음 실행에서 재확인 필요 |
| 잡 구성 | 1잡 직렬 → migration-smoke + browser-journeys (1)(2) + viewport-matrix, 동시 실행 |
| Actions 사용량(full 레인 합계) | 약 11분 → 이번 실행 합계 33.6분(321+437+728+529초, 그중 npm ci 지연 12.7분) · npm ci 정상 시 약 21분(≈1.9배) |
| 준비 단계 정본 | 워크플로 본문 → action 1곳 (ci:local과 대조 테스트가 action 파일을 읽음) |
| 단위 테스트 | ciLocal 15건·statsRefresh·ciScopePolicy·branchWorkflowOwnership 통과, npm run check 통과 |
| CI 1회 | run 33832020178 성공 — scope·verify·migration-smoke·browser-journeys (1)(2)·viewport-matrix 6잡 전부 통과. 샤드별 저니 205초/216초(31건을 파일 단위로 반씩, 각 샤드가 앱을 한 번씩 빌드), 뷰포트 105초 |
6. 이번 개선으로 향상된 것
PR을 열고 CI 결과를 기다리는 시간이 절반이 됐다
11분 → 약 6분. ci:local(#1225)로 PR 전에 대부분을 걸러내고, CI는 확인용 1회가 6분이면 한 트랙의 왕복이 눈에 띄게 짧아진다.
실패가 나뉘어 보인다
저니 한 건이 죽어도 어느 샤드인지, 뷰포트·DB 핵심은 통과했는지가 잡 단위로 남는다.
구조적으로 남는 것
- 로컬 Supabase 스택 준비 action 1개 — 다른 워크플로(예: prod-smoke)도 재사용 가능.
- 샤드 수 = matrix 한 줄.
남은 것
- 첫 실측이 목표에 못 미쳤다(12분 32초) — 원인은 이날
npm ci지연(전 잡 3분). Phase 4로 설치 캐시를 넣었다. 다음 full 실행이 캐시를 처음 채우고, 그 다음 실행부터 벽시계(구조 계산 약 6분)를 실측으로 확인한다 — 미검증. - 샤드 간 시간 편차 실측 뒤 필요하면 3샤드로.
prod-smoke.yml은 이 action을 아직 쓰지 않는다(Production 대상이라 로컬 스택이 없음 — 해당 없음).