Skip to content

승격·배포 실행 이름 통일과 QA1 퇴역·승격 CI 두 레인 (2026-09-15)

  • 기간: 2026-09-15 19:40 KST(오너 지시 "배포·검사 실행 이름을 [vX.Y.Z] … 4종으로 통일") ~ 2026-09-15 (세션 2개: 조사·계획 b22fa896, 구현 d326ddb8) · 이슈 #1659 Phase 1
  • 랜딩: 앱 PR #1660(release/v0.19.3 큐 병합 39bfb497) · QA1 PR #9(314d66cf, 앱 qa1.lock.json 핀) · 문서 = 이 저장소 PR. 마이그레이션·엣지 함수 없음, Vercel 배포 없음. Production 반영은 v0.19.3 릴리스와 함께
  • 설계서: 없음 — 이슈 #1659 본문 §3(해결안 A~D 비교, 채택 A)
  • 정본: 릴리스 프로세스 §승격·배포 실행 이름 · 앱 .github/workflows/production-deploy.yml·staging-deploy.yml·policy-contract.ymlrun-name · scripts/check-promotion.mjsrequireReleaseVersion · 저장소 변수 RELEASE_VERSION
  • 도구: 없음
  • 게이트: QA1 suite/tests/react/promotion.test.mjs(버전 추출·대조 검사 2건 추가)·promotionScope.test.mjs(fixture에 변수·제목 공급) · 앱 npm run check:static·check:deployment 통과. 실제 실행 이름의 첫 확인은 v0.19.3 승격 ① Full CI(Phase 2)
  • 버그리포트: 없음
  • 계약: 없음

Phase 현황

Phase내용상태
Phase 1Phase 1-1 실행 이름 규칙(앱 워크플로 3개·가드·변수 설정·문서) · Phase 1-2 QA1 퇴역 + 승격 CI 두 레인(후속 #1664·#1667·#1669)✅ 앱 PR #1660 (39bfb497) · PR #1663
Phase 2승격 ① release/v0.19.3 → staging✅ 승격 PR #1670 병합 73a34e2d, Staging Deploy 34968896451 성공(DB·Edge·프런트)
Phase 3승격 ② staging → main(Production) · 포함 이슈 종결✅ 릴리스 PR #1671 관리자 병합 dd7d2828(오너 지시), Production Deploy 34973782621 성공, 태그 v0.19.3, 포함 이슈 13건 종결

1. 배경

릴리스 경로는 작업 → release/vX.Y.Zstagingmain이고, 한 릴리스가 나가는 동안 Actions 목록에는 실행 네 개가 순서대로 생긴다: 승격 ① PR의 GitHub Full CI, staging 배포, 승격 ② PR의 GitHub Full CI, Production 배포. 오너가 2026-09-15 19:40 KST에 "배포·검사 실행 이름을 [vX.Y.Z] Release -> Stage Merge · [vX.Y.Z] Staging Deploy · [vX.Y.Z] Stage -> Main Merge · [vX.Y.Z] Production Deploy 4종으로 통일"하라고 지시했고, v0.19.3 릴리스 준비(#1659)의 첫 Phase로 실었다.

2. 문제 제기

실행 이름이 워크플로마다 다른 규칙으로 정해져 어느 릴리스의 어느 단계인지 목록만 봐서는 알 수 없었다

  • Full CI 실행은 PR 제목을 그대로 썼다: [Release v0.19.1] www 루트 주소가…, staging → main: #1612 Phase 3….
  • 배포 실행은 병합 커밋 첫 줄을 썼다: Merge pull request #1622 from dekerd/staging. 버전이 어디에도 없다.
  • 원인은 GitHub의 구조다. 실행 이름(run-name)은 실행이 만들어지는 순간 정해지고, 그 자리에서 쓸 수 있는 값은 이벤트 정보(브랜치 이름·커밋 메시지·PR 제목)·수동 입력·저장소 변수뿐이며 문자열을 잘라내는 함수가 없다. release/v0.19.3에서 v0.19.3만 꺼낼 수 없고, push로 도는 배포 실행에는 PR 정보 자체가 없다.

3. 해결 방안

원칙 (오너 지시 2026-09-15 19:40 KST)

"배포·검사 실행 이름을 [vX.Y.Z] … 4종으로 통일" — 채택. 워크플로 표시 이름(1-Production Deploy 등, 2026-09-09 결정)과 큐 실행 이름은 지시 범위 밖이라 그대로 둔다.

접근

내용결과채택
A. 저장소 변수 RELEASE_VERSION승격을 시작할 때 "지금 나가는 릴리스 버전"을 변수 하나에 적고, 네 실행 이름이 전부 그 값을 읽는다. 변수를 안 바꾸면 이름이 틀리므로 Full CI의 첫 잡(scope)이 변수와 승격 PR의 버전을 대조해 다르면 실패시키고 고치는 명령을 안내한다4종 이름이 정확히 일치. 변수 누락은 [v?]로 드러나고 가드가 막는다채택
B. PR 제목을 실행 이름으로승격 PR 제목을 실행 이름 형식으로 쓰고 Full CI가 제목을 이름으로Full CI 2종만 해결. push로 도는 배포 2종은 PR 제목을 못 본다불채택
C. 배포를 "PR 병합됨" 이벤트로배포 워크플로가 PR 정보를 볼 수 있게 트리거 변경main 직접 push(관리자 우회)가 배포되지 않는 등 동작이 달라진다. 릴리스 직전에 넣을 변경이 아니다불채택
D. 라우터 워크플로push마다 작은 워크플로가 버전을 계산해 배포 워크플로를 입력과 함께 다시 실행실행이 2개로 늘고 봇 토큰이 필요불채택

4. 적용한 내용

Phase 1 — 실행 이름 규칙 (앱 PR #1660, QA1 PR #9, 문서 PR)

앱 저장소:

  • .github/workflows/production-deploy.yml · staging-deploy.yml: <code>run-name: "[${{ vars.RELEASE_VERSION || 'v?' }}] Production Deploy"</code> / … Staging Deploy.
  • .github/workflows/policy-contract.yml: PR base가 main이면 Stage -> Main Merge, 아니면 Release -> Stage Merge. 수동 실행(workflow_dispatch)만 (manual) 꼬리. scope 잡 env에 RELEASE_VERSION 전달.
  • scripts/check-promotion.mjs: releaseVersionFor(release→staging은 head 브랜치 release/vX.Y.Z, staging→main은 제목 [Release vX.Y.Z]에서 버전) · requireReleaseVersion(변수와 다르면 실패 — 메시지에 gh variable set RELEASE_VERSION --body vX.Y.Z --repo dekerd/Barbelic와 "push하거나 PR을 닫았다 열어 새 실행을 만들라"를 적는다). checkPromotion이 lane 판정 직후 호출.
  • qa1.lock.json → QA1 314d66cf(suite tree 905f7621).
  • 저장소 변수 RELEASE_VERSION=v0.19.3 설정(2026-09-15 20:12 KST, 실행을 일으키지 않음).

QA1 저장소(PR #9, 분기 기준 = 앱이 고정하던 c65d2fa5): promotion.test.mjs에 버전 추출·대조 검사 2건, promotionScope.test.mjs fixture에 RELEASE_VERSION·PR 제목 공급.

문서 저장소: 릴리스 프로세스 §2 승격 ①에 변수 설정 단계, "CI와 배포 실행 기준"에 실행 이름 표(#run-names), 배포 파이프라인 2026-09-09 개정 주석에 2026-09-15 개정 한 줄, 이 작업 기록.

Phase 1-2 — QA1 퇴역·승격 CI 두 레인 (앱 PR #1663)

오너 지시(2026-09-15 채팅): "release->stage: precheck만, stage->main: QA2만", "QA1은 이번 배포부터 완전히 deprecated(워크플로에서 자동실행되지 않도록)". 계기 = release/v0.19.3에서 QA1 고정 단위 테스트 4,061건 중 123건이 #1610의 앱 구조 변경을 따라가지 못해 실패하고 있었고(승격 ① Full CI가 그대로 빨간불), QA1은 곧 폐기 예정이라 그 갱신에 시간을 쓰지 않기로 한 것.

  • policy-contract.yml: scope 잡이 lane(staging / production / manual)을 내고, release→staging = precheck 레인(static-checks + migration-smoke: 마이그레이션 전체 재적용·schema.sql 대조), staging→main = QA2 레인(qa2-product-contracts), 수동 실행 = 셋 다. QA1 잡(단위·브라우저·viewport·e2e-evidence)과 migration-smoke의 pgTAP·tests/db·로컬 e2e 단계 삭제. verify는 레인 안 잡 성공 + 레인 밖 잡 skipped를 요구.
  • check-promotion.mjs: 동일 tree full 성공 기록 재사용 제거. staging 배포 증거 = database·functions·frontend 3잡 성공(QA1 smoke 잡 없음).
  • deploy-steps.yml: 배포 뒤 QA1 CRUD smoke 잡·browser-journeys(-report)·QA1_DEPLOY_KEY·prepare-qa1 삭제, release-tagfrontend 뒤. 큐(release-merge-request.yml)·백업(daily-data-backup.yml)·관리자 fixture 워크플로의 prepare-qa1 삭제. prod-smoke.yml·social-login-daily-check.yml(QA1 e2e 전용) 삭제.
  • 앱 저장소: .github/actions/prepare-qa1·scripts/qa1.mjs·qa1.lock.json 삭제, npm 자동 훅 pre* 31개·qa1:*·test·test:e2e-*·smoke:* 삭제, check = check:static, check:static에서 QA1 디렉터리를 읽는 check:test-manifest·check:error-cases 제외. ci-local.mjs는 QA1 e2e 헬퍼를 browser 단계에서만 지연 로드(큐 요청 스크립트가 QA1 파일 없이 import 되게).
  • 같이 고친 것: #1610 병합 커밋이 남긴 미사용 React import 1줄(friendScopeController.ts) — release/v0.19.3에서 check:unused가 실패하고 있었다.
  • 문서: 릴리스 프로세스(재사용 절 개정 주석·§2/§3 단계·CI 표), 배포 파이프라인·사전검증·QA1 저장소에 퇴역 주석.

주요 결정과 그 근거

  • 변수 하나로 4종을 맞춘다(안 A): push 이벤트에서도 읽을 수 있는 값은 저장소 변수뿐이다. staging에 후보 하나만 두는 현 정책과 같은 가정("지금 나가는 릴리스는 하나")이라 값도 하나로 충분하다.
  • 가드는 scope 잡에: 변수 설정을 잊으면 이름만 틀리는 것이 아니라 Full CI가 첫 잡에서 멈추게 해, 사람이 이름을 눈으로 확인하지 않아도 잡힌다. 안내 문구에 정확한 명령을 넣는다.
  • 재실행이 아니라 새 실행: 실행 이름은 실행 생성 시점에 고정되므로 가드 실패 뒤 "Re-run"은 이름을 못 바꾼다. 안내에 "push 또는 PR 닫았다 열기"를 명시했다.
  • 수동 실행 꼬리 (manual): 오너 지시 4종 밖의 유일한 예외. 진짜 게이트와 진단용 수동 실행을 목록에서 구분하기 위함.
  • "precheck" = static-checks + migration-smoke(QA1 단위 테스트 제외): 사전검증의 정의에는 단위 테스트가 들어 있었지만 그 테스트 전부가 QA1 소유라 "QA1 완전 퇴역" 지시와 함께 빠진다. 배포 뒤 CRUD smoke도 QA1 e2e라 함께 퇴역 — staging→main의 QA2 레인이 제품 계약을 검증한다.
  • QA1 기반 크론·수동 워크플로 삭제: 소셜 로그인 일일 점검·수동 prod-smoke는 QA1 e2e 없이는 돌 수 없어 남겨 두면 실패만 한다. 필요하면 QA2로 다시 만든다(오너 결정).

Phase 2 — 승격 ① release/v0.19.3 → staging

승격 PR은 세 번 열렸다(코드가 바뀔 때마다 새 후보). 모두 precheck 레인(scope·static-checks·migration-smoke·verify) 4/4 성공.

회차승격 PRCI배포결과
1#1662 (7634dd9f)3496602324934966525792database 잡 실패 — DB 롤아웃 스크립트가 QA1 소유 ci-local.mjs import(DB 미적용) → #1667
2#1668 (4878c7f5)3496711193134967610368database ✅ functions ✅ frontend ❌ — Vercel 배포는 성공, 배포 직후 1회 읽는 "공개 주소 검증"이 옛 번들을 보고 실패(몇 초 뒤 새 번들 서비스 중) → #1669
3#1670 (73a34e2d)3496837279134968896451database·functions·frontend 성공. staging tree = release tree add762b4, staging.barbelic.com이 이 릴리스 번들 서비스

Phase 3 — 승격 ② staging → main (Production)

릴리스 PR #1671([Release v0.19.3] …, QA2 레인)은 세 번 실패한 뒤 오너 지시로 관리자 병합했다.

실행결과원인조치
34969921420qa2-product-contracts ❌ (QA2 자체 검사 91번)QA2 terminateOwned가 SIGKILL 직후 돌아와 좀비를 "살아 있음"으로 판정(kill(pid, 0)). ubuntu 러너에서 반복(취소된 34963508731도 동일), node:22 컨테이너 3/3 재현QA2#7 소유 pid 소멸/회수까지 최대 2초 대기 + 좀비 인식 판정, 앱 핀 #1672
34971571742scope ❌staging push 2초 뒤 자동 실행이 아직 도는 Staging Deploy를 "성공 기록 없음"으로 봄(순서 경쟁)PR 닫았다 열어 새 실행
34972045502qa2-product-contracts ❌ (제품 계약 단계 exit 3)Cached PostgreSQL image differs from pinned content identity — 고정값 be60aee1…은 레지스트리 manifest 다이제스트인데 판정은 docker image inspect .Id. Docker Desktop(containerd 저장소)에서는 같지만 GitHub 러너(classic overlay2)는 config 다이제스트(9d0c8f4d…)를 돌려줌. hosted에서 이 단계가 실제로 돈 첫날QA2#8 RepoDigests@고정값도 인정, 앱 핀 #1675
(오너 지시 "다 관리자 권한으로 머지")남은 재승격 CI 취소#1676(release→staging)·#1671(staging→main) --admin 병합 → staging Deploy 34973721922 ✅, Production Deploy 34973782621 ✅(database·functions·frontend·release-tag)

결과: main dd7d2828 = staging dd2e1f39 = release fd5f4c7f(tree 86b4318d). app.barbelic.com이 이 릴리스 번들을 서비스. QA2 제품 계약 단계는 hosted에서 끝까지 돈 적이 없다(두 수리는 그 앞 단계 결함이었고, 수리 뒤 실행은 오너 지시로 취소) — 이 릴리스의 제품 판정은 staging·Production 배포 검사(DB 마이그레이션 적용·Edge·프런트 공개 주소 검증)뿐이다.

작업 중 드러난 것

  • run-name 식은 GitHub이 실행을 만들 때 평가하므로 틀리면 워크플로가 시작조차 못 한다(startup failure). 로컬에서 @actions/expressions(GitHub의 공식 식 파서)로 세 식을 파싱·평가해 네 이름과 (manual) 꼬리, 변수 누락 시 [v?]를 확인했다. 실제 GitHub에서의 첫 확인은 v0.19.3 승격 ①.
  • 앱의 npm run check:static은 시작 시 QA1 핀을 준비하는데, 워크트리 tests/에 손으로 복사한 테스트가 남아 있으면 "QA1 preserves a local modification"으로 멈춘다. 핀을 새 QA1 커밋으로 올린 뒤 다시 돌려야 한다.
  • 문서 저장소 main이 계약 문서 render-invariants.md(#1610 docs PR #119)의 표 안 인라인 코드 <code>x={{</code>로 VitePress 빌드가 깨져 있었다(Vue가 <code>{{</code>를 보간 시작으로 읽음, docs CI 실패·Docs Deploy skipped). 이 문서 PR이 통과하려면 빌드가 돼야 하므로 그 두 글자만 HTML 엔티티(&#123;&#123;)로 바꿨다 — 화면 표시는 그대로 <code>x={{</code>.
  • 사전 검증 누락 2건(2026-09-11 규칙에 따라 기록): ① #1664에서 "QA1 파일이 없는 워크트리"로 static-checks만 재현하고 배포 워크플로가 실행하는 스크립트(release-v0171-projection-rollout.mjs)의 QA1 import를 대조하지 않아 staging 배포 1차가 DB 롤아웃 단계에서 멈췄다(DB 미적용). 뒤늦게 추적 스크립트 전체를 "미추적 파일 import" 기준으로 기계 스캔해 자동 실행 경로의 잔여를 0으로 만들었다. ② 배포 뒤 공개 주소 검증이 1회만 읽어 도메인 전환·엣지 캐시 반영보다 앞서 실패(staging.barbelic.com은 몇 초 뒤 새 번들 서비스 중) — 2분 재확인·캐시 우회로 고정.
  • QA2 게이트의 첫 실전(hosted) 실행에서 도구 결함 2건(제품 아님): 실행기 자기 검사의 Linux 좀비 판정, PostgreSQL 이미지 고정 판정의 Docker 저장소 의존. 둘 다 QA2 저장소에서 수리했고 앱 핀은 9f4ceee9. 제품 계약 단계 자체의 hosted 통과 실적은 아직 없다 — 다음 릴리스의 첫 관문.
  • 릴리스 PR을 열어 둔 채 staging을 갱신하면 CI가 배포보다 먼저 시작해 scope가 실패한다. 릴리스 PR은 staging 배포 성공 뒤에 열거나(또는 잠시 닫았다가 다시 열기), scope가 진행 중인 Deploy를 기다리게 바꿔야 한다 — 별도 이슈 권장.
  • 빈 QA2 핀(commit: "")을 담은 PR #1674를 잘못 열어 큐에 넣었다가 즉시 취소·닫음(release 미반영). 원인은 명령 연쇄의 조건 검사가 테스트 리포터 출력 형식을 잘못 가정한 것 — 핀 값은 커밋 전 정규식으로 검사하도록 바꿨다.
  • QA1 node bin/qa1.mjs verify는 앱이 고정하던 c65d2fa5에서 이미 "Unrecorded migration change: e2e/profile-design-settings.spec.mjs"로 실패한다(다른 세션의 브랜치 커밋, QA1 main은 통과). 이 변경과 무관해 손대지 않았다.

5. 적용 결과

항목
승격 ① Full CI 실행 이름PR 제목(release/v0.19.3 → staging …)[v0.19.3] Release -> Stage Merge
staging 배포 실행 이름병합 커밋 첫 줄(Merge pull request #N …)[v0.19.3] Staging Deploy
승격 ② Full CI 실행 이름PR 제목([Release v0.19.3] …)[v0.19.3] Stage -> Main Merge
Production 배포 실행 이름병합 커밋 첫 줄[v0.19.3] Production Deploy
변수 누락 시없음(알 수 없음)이름 [v?] + scope 잡 실패·명령 안내
가드 검사없음QA1 promotion 검사 24건 통과(추가 2건 포함; 이후 QA1 퇴역으로 CI에서는 돌지 않음)
승격 ① CI 잡scope + static-checks + unit-tests(2) + migration-smoke + browser(4) + viewport + e2e-evidence + qa2 (약 15분, QA1 실패 123건으로 빨간불)scope + static-checks + migration-smoke + verify
승격 ② CI 잡동일 tree 기록 재사용 또는 fullscope + qa2-product-contracts + verify
실행 이름 실측(v0.19.3)4종 전부 확인: [v0.19.3] Release -> Stage Merge · [v0.19.3] Staging Deploy · [v0.19.3] Stage -> Main Merge(34969921420 등) · [v0.19.3] Production Deploy(34973782621)
배포 뒤 공개 주소 검증1회 읽기(전환 지연에 실패)2분간 10초 간격·캐시 우회
배포 뒤 검증database → functions → frontend → smoke(QA1 CRUD)database → functions → frontend (→ release-tag)
QA1 자동 실행 지점워크플로 8개·npm 훅 31개0

미검증: QA2 제품 계약 단계의 hosted 통과(위 Phase 3 표). Production 배포의 공개 주소 2분 재확인은 34973782621에서 통과.

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

  • Actions 목록에서 릴리스 버전과 단계가 바로 읽힌다(오너 체감).
  • 변수 누락으로 이름이 틀리는 일을 기계가 막고 고치는 명령까지 안내한다.
  • 규칙이 문서 정본(§run-names)과 코드(가드·테스트)에 함께 남아, 다음 릴리스 담당 세션이 승격 ① 전에 변수 설정 단계를 빠뜨리지 않는다.

남은 것

  • QA2 제품 계약 단계의 hosted 첫 통과 확인(다음 승격 ②에서). 릴리스 PR 순서 경쟁(scope가 진행 중인 Staging Deploy를 기다리도록) 수리 이슈.
  • QA1 퇴역으로 없어진 검증(단위 4,061건·pgTAP·브라우저 e2e·배포 smoke·소셜 로그인 일일 점검)의 대체는 QA2 범위 결정에 따른다. ci:precheck-local·ci:full-local·merge:request --dry-run의 QA1 단계는 실행되지 않는다(scripts/ci-local.mjs 정리는 별도).
  • 릴리스 사이에 main에 직접 들어간 변경(문서·QA 핀)의 Production 배포도 직전 릴리스 버전으로 표시된다 — 4종 통일의 대가로 수용.