Repository navigation
fix(ci): 릴리스 QA 구조화 입력 게이트 — PR 단위 QA 원천 강제 (#809 1·2) [base: develop] - #810
Conversation
Sadturtleman
left a comment
There was a problem hiding this comment.
구조화 QA 원천을 강제하는 방향 자체에는 동의합니다 — generic fallback 문구가 QA 를 통과하던 구멍을 닫는 게 #809 의 요지고, qa-semantic-audit.mjs 의 strict validation 과 fail-closed(human_review_required → hold, 배포 미실행) 설계도 맞습니다.
다만 전개 비용이 측정되지 않은 채로 필수 검사에 붙습니다. 그 1건만 적습니다.
[blocking] 이 게이트가 현재 열린 PR 48개 중 47개를 떨어뜨립니다
validate-pr-qa-metadata.mjs 를 이 브랜치에서 꺼내 실제 열린 PR 본문 전수에 돌려 봤습니다.
PASS = 1 (#810 — 이 PR 자신)
FAIL = 47 886 885 883 882 880 879 878 877 876 874 871 870 869 868 867 865 864
863 862 861 857 856 855 854 853 834 833 832 831 830 829 828 827 826
825 824 823 822 821 820 819 807 771 767 741 721 440
전부 같은 사유입니다.
::error title=구조화 QA 메타데이터 오류::PR #877: `QA 메타데이터` 섹션이 없습니다.
재현 방법 (이 브랜치 기준):
gh pr view <N> --json number,body \
--jq '{pull_request:{number:.number, body:.body}}' > event.json
node .github/scripts/validate-pr-qa-metadata.mjs event.jsonunit-test.yml 에서 이 단계가 Run deployment script tests 앞, 즉 Unit Test job 의 첫 검증으로 들어가 있어 실패하면 job 전체가 빨간불이 됩니다.
당장 47개가 동시에 빨개지진 않습니다 — 다만 시간 문제입니다
pull_request 이벤트는 워크플로 정의를 head 브랜치에서 읽으므로, 기존 PR 들은 develop 을 끌어오기 전까지 옛 unit-test.yml 로 계속 돕니다. 문제는 이 레포가 리베이스를 매우 자주 한다는 겁니다 — 당장 #819→#831 스택 13개는 앞 PR 이 머지될 때마다 순차로 리베이스해야 하고, 그 순간부터 본문을 고치기 전엔 머지가 막힙니다.
부담이 작지 않습니다
ci-only / covered-by-ci 로 빠지는 경우에도 exclusionReason 과 동일 input·boundary·observation 을 단언하는 ci/test evidence 1건 이상이 필요합니다(qa-semantic-audit.mjs:232-241). #871 처럼 gradle.properties 중복 한 줄을 지우는 PR 에도 같은 블록을 채워야 합니다.
제안
셋 중 하나면 충분해 보입니다.
- 한시적 warn-only — 일정 기간
::warning::으로만 내고process.exitCode를 세우지 않다가 기존 PR 이 소진되면 켭니다. #833 이 "경고만 남길 거면 워크플로를 지우는 편이 정직하다" 고 한 것과 상충해 보일 수 있는데, 저기는 영구 경고고 여기는 전환 기간이라 성격이 다릅니다. - 기준 시점으로 grandfather — PR 생성 시각이나 번호가 컷오프 이전이면 건너뜁니다. 스크립트에
event.pull_request.created_at이 이미 들어와 있어 몇 줄이면 됩니다. - 머지 전 일괄 정비 — 47개 본문을 먼저 채우고 게이트를 켭니다. 가장 깨끗하지만 이 PR 이 그만큼 대기합니다.
어느 쪽을 택하실지, 혹은 47개 동시 정비를 이미 감안하신 것인지만 알려주시면 다시 보겠습니다. 의도된 비용이라면 그 판단을 존중합니다 — 다만 PR 본문에 근거가 없어 리뷰에서 확인할 방법이 없었습니다.
그 밖에 확인한 것
PULL_REQUEST_TEMPLATE.md에app-runtime기본형과 제외 경로 안내를 함께 넣으신 것 — 신규 PR 은 템플릿만 채우면 통과합니다. 파일 끝 개행도 같이 정리됐고요.validatePullRequestEvent가pull_request가 아닌 이벤트에서skipped로 빠져 push·schedule 실행을 막지 않습니다.escapeWorkflowCommand가%·CR·LF 를 이스케이프해 여러 줄 오류가 annotation 을 깨지 않습니다.- Copilot token·호출·응답 검증 중 하나라도 실패하면
hold로 닫고 Firebase 배포를 실행하지 않는 fail-closed 구조. - 모델이 정본의 사전조건·행동·기대 결과·위험·근거를 수정하지 못하게 strict validation 을 건 것 — 감사 대상이 감사 결과를 고칠 수 있으면 감사가 아니게 되니 맞는 제약입니다.
- Node 테스트 28개와
#805기준점 fixture.
|
리뷰 반영했습니다 — ecfc9aa. 제안 2번(grandfather)을 택했습니다. [blocking] 열린 PR 47/48 차단
실측 — 말씀하신 방법대로 현재 열린 PR 전수(46개)의 실제 본문·생성 시각을 수정된 검증기에 돌렸습니다:
컷오프 이후 생성되고 head가 새 워크플로를 가진 PR부터 강제되는데, 기본 브랜치가 develop이라 이 PR이 머지되는 즉시 새 PR 템플릿이 노출되어 신규 PR은 템플릿만 채우면 됩니다. |
|
CI 참고 — 방금 실패한 Run Unit Tests는 이번 반영 커밋(ecfc9aa)과 무관합니다.
|
|
#842 불변식 충돌을 해소했습니다 — 9672f2c (develop 머지 5410990 포함). deployment-decision.yml을 develop push 트리거로 전환pull_request 워크플로에
검증
|
|
이 PR 은
|
develop 이 deployment-decision.yml 을 걷어냈다 (d657a04). 판정 16건이 전부 같은 값이었고 dispatch 실행이 3건뿐이라 "판정은 쓰이지 않았다" 는 실측이 근거였고, 배포 시점은 릴리스 PR 을 여는 사람이 정하는 것으로 확정됐다. 이 브랜치의 AI 의미 감사(#809 완료조건 3)는 그 판정 위에 얹혀 있었다. 토대가 사라졌고, COPILOT_PERSONAL_TOKEN 도 미등록이라 지금 머지해도 항상 human_review_required 로만 끝난다. 그래서 감사 쪽을 걷어낸다. - 삭제: deployment-decision.yml, evaluate-deployment-decision.mjs(+test) - 삭제: qa-semantic-audit 의 감사 절반 — Copilot 호출·plan strict validation· finalizeDeploymentDecision·PR #802 fixture - collect-release-scope-context.mjs 는 develop 판(릴리스 PR 기준 필드) 채택. 게이트는 GITHUB_EVENT_PATH 만 읽어 이 파일과 무관하다. 남는 것은 완료조건 1·2 다 — PR 단위 구조화 QA 원천과 결정적 게이트. develop 이 "QA 포인트는 사람이 쓴다" 로 간 만큼 그 입력 품질을 PR 에서 강제하는 쪽이 오히려 서로 보완한다. - qa-semantic-audit.mjs 777줄 -> qa-metadata.mjs 230줄 (파싱·검증만, 죽은 심볼 제거) - qa-semantic-audit.test.mjs 14개 -> qa-metadata.test.mjs 8개 (감사 테스트 6개 제거) - validate-pr-qa-metadata.mjs, unit-test.yml 게이트, PR 템플릿, 릴리스 노트의 generic fallback 차단은 그대로 grandfather cutoff 를 8/24 -> 8/26 으로 민다. 8/24 로 두면 그 뒤 열린 PR 22건이 리베이스하는 순간 막힌다 (전수 실측). 8/26 기준으로는 열린 PR 53건 중 FAIL 0건 — #810 자신만 검증을 받고 나머지 52건은 건너뛴다. 검증: node --test .github/scripts/*.test.mjs 110/110, 워크플로 YAML 18개 파싱, 열린 PR 53건 전수 게이트 실행.
|
@Sadturtleman @koongmai 재리뷰 부탁드립니다. 지적하신 blocking 과 별개로 PR 범위가 크게 줄어서, 본문을 다시 쓰고 제목도 바꿨습니다. 1. AI 의미 감사를 철회했습니다 (1,814줄 → 575줄)리뷰해 주신 뒤에 develop 이 이 PR 의 감사는 그 판정 위에 얹혀 있었습니다. 토대가 사라졌고 README 에 "별도 API나 유료 AI를 호출하지 않는다" 가 방침으로 박혀서, 감사 쪽(Copilot 호출·plan strict validation·
남은 건 완료조건 1·2 인 구조화 QA 원천과 게이트입니다. 2. blocking — cutoff 를 다시 밀었습니다2안(grandfather)으로 갔는데, cutoff 가 재현 절차에 한 가지 정정이 있습니다. gh pr view <N> --json number,body,createdAt --jq '{pull_request:{number:.number, body:.body, created_at:.createdAt}}' > event.json
node .github/scripts/validate-pr-qa-metadata.mjs event.json머지가 8/26 을 넘기면 넘긴 만큼 다시 밀어야 합니다. 그 뒤로는 템플릿을 채운 신규 PR 만 대상이 됩니다 (템플릿을 그대로 내면 placeholder 라 떨어집니다 — 의도한 동작입니다). 3. 검증CI 에서 게이트가 처음 실제로 돌았고 통과했습니다. 이 PR 자신이 게이트 대상이라, 본문의 QA 메타데이터가 검증을 받은 결과입니다.
앱 프로덕션 코드는 건드리지 않습니다 — |
|
제안 2번(기준 시점 grandfather) 으로 갔고, 말씀하신 대로 grandfather 컷오프// validate-pr-qa-metadata.mjs:15
export const QA_METADATA_GATE_CUTOFF = "2026-08-26T00:00:00Z";
function isGrandfathered(pullRequest) {
const createdAt = Date.parse(pullRequest?.created_at ?? "");
return Number.isFinite(createdAt) && createdAt < Date.parse(QA_METADATA_GATE_CUTOFF);
}컷오프 이전에 열린 PR 은 전수 재실측 — 47건 → 0건리뷰에 주신 재현 방법 그대로, 다만 gh pr list --state open --limit 100 --json number,body,createdAt
# → {"pull_request":{number, body, created_at}} 로 변환 후
node .github/scripts/validate-pr-qa-metadata.mjs events/<N>.json지적하신 «#819→#831 스택 13개가 순차 리베이스하는 순간 막힌다» 도 이 컷오프 아래에서는 발생하지 않습니다. 다만 컷오프가 머지일보다 뒤여야 합니다
머지가 밀리면 밀린 만큼 컷오프를 다시 밀겠습니다. 확인 부탁드립니다. 범위 축소AI 의미 감사는 이 PR 에서 철회하고 QA 메타데이터 게이트만 남겼습니다(95efbdd). |
- develop 이 제거한 `develop` 수동 배포 섹션을 따르고, 그 입력 설명에 있던 generic fallback 거부 규칙은 살아남은 main 경로 설명으로 옮겼다 (검증 자체는 render-distribution-release-notes.sh 에 그대로 남아 있다) - 이 PR 이 신설한 'PR별 구조화 QA 원천' 섹션은 유지
컷오프가 8/26 00:00Z 라 그 뒤 열린 #1194 가 다시 게이트에 걸렸다. «머지가 이 날짜를 넘기면 넘긴 만큼 다시 민다» 는 주석의 결정대로 하루 민다. 실측: 열린 PR 34건 전수 PASS 34 / FAIL 0, qa-metadata.test.mjs 8/8.
|
컷오프가 밀려 지적이 다시 열려 있던 것을 닫았습니다 (
실측 — 이 브랜치의 검증기를 열린 PR 전수에 돌린 결과 gh pr list --state open --limit 200 --json number,body,createdAt > openprs.json
# PR 별로 event.json 을 만들어
node .github/scripts/validate-pr-qa-metadata.mjs event.json
리뷰 때 지적하신 47건은 grandfather 도입(#810 |
Dismissed because a later approving review from another current write-capable reviewer is the repository merge decision.
📌𝘐𝘴𝘴𝘶𝘦𝘴
Closes #809
이슈 범위를 이 PR 의 범위(PR 단위 구조화 입력 게이트)로 축소했다 — 원 완료조건 3·4 는 토대가 사라져 제외했고, 근거는 #809 코멘트에 남겼다.
📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯
QA_METADATA_GATE_CUTOFF). 리베이스로 워크플로를 받아도 소급 차단되지 않는다.AI 의미 감사(#809 원 완료조건 3·4)를 범위에서 뺀 이유
develop 이
deployment-decision.yml을 걷어냈다 (d657a04). 판정 16건이 전부 같은 값이었고 dispatch 실행이 3건뿐이라 "판정은 쓰이지 않았다" 는 실측이 근거였고, 배포 시점은 릴리스 PR 을 여는 사람이 정하는 것으로 확정됐다. 이 PR 의 AI 감사는 그 판정 위에 얹혀 있었다.토대가 사라졌고
COPILOT_PERSONAL_TOKEN도 미등록이라, 지금 머지해도 항상human_review_required로만 끝난다. 그래서 감사 쪽(Copilot 호출·plan strict validation·finalizeDeploymentDecision·PR #802 fixture, 약 1,400줄)을 걷어내고 게이트만 남겼다.남은 게이트는 develop 방향과 오히려 보완적이다 —
release-scope.yml이 "QA 포인트는 비어 있을 때만 초안을 채우고 사람이 쓴 문장은 보존" 으로 갔으므로, 그 초안의 원천 품질을 PR 단위에서 강제하는 쪽이 값이 있다.qa-semantic-audit.mjsqa-metadata.mjs230줄 (파싱·검증만)🧪 QA Metadata
{ "scope": "ci-only", "exclusionReason": "PR 검증 워크플로와 릴리스 노트 렌더러만 바꾸며 APK 사용자 흐름은 변경하지 않는다", "evidence": [ { "kind": "test", "ref": ".github/scripts/qa-metadata.test.mjs", "assertion": "구조화 필드 누락·generic fallback·제외 근거 부족을 실패로, cutoff 이전 생성 PR 을 건너뜀으로 고정한다", "input": "runnable·exclusion 메타데이터와 created_at 이 cutoff 앞뒤인 pull_request 이벤트", "boundary": "본문 섹션 파싱부터 워크플로 오류 annotation 과 종료 상태까지", "observation": "node --test 가 110개 스크립트 테스트 전부의 통과를 단언한다" }, { "kind": "ci", "ref": "Unit Test / Validate structured QA metadata", "assertion": "열린 PR 전수에서 게이트가 의도한 대상만 검증한다", "input": "열린 PR 53건의 실제 본문·생성 시각", "boundary": "grandfather 판정부터 구조화 필드 검증까지", "observation": "FAIL 0건 — 이 PR 만 검증을 받고 나머지 52건은 건너뛴다" } ] }📷𝘚𝘤𝘳𝘦𝘦𝘯𝘴𝘩𝘰𝘵
💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴
@Sadturtleman 지적하신 blocking — 게이트가 열린 PR 을 무더기로 떨어뜨리는 문제입니다. 2안(grandfather)으로 갔고, cutoff 를 실측해서 다시 밀었습니다.
8/24 로 두면 그 뒤 열린 PR 22건이 리베이스하는 순간 막힙니다. 8/26 으로 밀어 열린 PR 53건에 전수로 돌린 결과입니다.
재현 (이 브랜치 기준):
created_at을 넘겨야 grandfather 가 동작합니다 — 이전 재현 절차에는 그 필드가 빠져 있어 전부 FAIL 로 보였습니다.머지가 8/26 을 넘기면 넘긴 만큼 cutoff 를 다시 밀어야 합니다. 그 뒤로는 템플릿을 채운 신규 PR 만 대상이 됩니다 (템플릿을 그대로 내면 placeholder 라 떨어집니다 — 의도).
검증:
node --test .github/scripts/*.test.mjs— 110/110