Skip to content

[CI] 전국 후보 갱신을 CI 자동 PR로 만들고 후보 병합 시 release-candidate를 자동 실행 - #928

Merged
AquilaXk merged 6 commits into
mainfrom
ci/nationwide-candidate-refresh-chain-927
Oct 4, 2026
Merged

AquilaXk merged 6 commits into
mainfrom
ci/nationwide-candidate-refresh-chain-927

Conversation

@AquilaXk

@AquilaXk AquilaXk commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Related issue

Refs #927
Refs #870

Summary

  • Problem:
    • 전국 후보 갱신(refresh-nationwide-candidate.mjs)을 지금은 에이전트가 로컬에서 실행하고 산출물을 손으로 커밋한다. [Refactor] 데이터 파이프라인 운영 방식 전환: 자동 갱신·개발/발행 게이트 분리·생성 산출물 축소 #870 결정(도구 PR과 데이터 PR 분리, 데이터 PR은 자동 생성 PR만)과 맞지 않는다.
    • release-candidate(RC)는 매번 사람이 datapack-release.yml을 dispatch한다. 그런데 modeArgs는 모두 저장소 파일로 정해진다(마지막 RC run 37109648483의 modeArgs와 같다).
    • hub datapack-promotion.yml은 후보 run의 event가 workflow_dispatch여야 받는다. 그래서 RC를 push 이벤트로 바로 돌리면 promotion에서 거부된다.
    • GITHUB_TOKEN으로 만든 PR에는 pull_request CI가 붙지 않는다. 그래서 required check가 영영 생기지 않는다.
  • Outcome:
    • Nationwide Candidate Refresh(dispatch 1회)가 CI에서 후보를 다시 만들고 draft PR을 연다. 그 PR head에 ci.yml을 dispatch해 required check를 붙인다.
    • 그 PR이 main에 들어오면 Data Pack Release Candidate Chain이 RC를 workflow_dispatch로 실행한다.

Changes

  • tools/ci/plan-datapack-release-chain.mjs(신규)
    • candidate-refresh
      • releaseSequence는 커밋된 후보보다 커야 한다.
      • 요청 역할과 승인 역할은 달라야 한다(대소문자 무시). 역할 값은 [A-Za-z0-9][A-Za-z0-9._-]{0,63} 토큰만 받는다.
      • 후보 범위는 nationwide_routing_android_v1만 받는다.
      • 후보 시계는 실행 시각(UTC ms)이다. 결과는 GitHub output에 쓴다.
    • release-candidate-mode-args
      • 기존 releaseRequestBindingViolations를 release-request-<candidateId> 기대값과 함께 그대로 쓴다. RC workflow와 같은 술어다.
      • approvalId 토큰 형식, 범위, RC 증거 파일(android evidence, strict route regression) 존재를 확인한 뒤에만 modeArgs를 쓴다.
  • .github/workflows/nationwide-candidate-refresh.yml(신규)
    • workflow_dispatch 전용, main 전용이다.
    • dispatch 입력은 env로만 run에 넘긴다(셸 주입 차단).
    • 처리 순서: 입력 검증 → 후보 갱신 → 출력 경로 검사 → automation/927-nationwide-candidate-refresh-<run> 브랜치 push → draft PR → ci.yml dispatch
    • 출력 경로 검사(Verify candidate refresh output scope, 리뷰 F4): NATIONWIDE_CANDIDATE_REFRESH_OUTPUTS 밖의 변경이나 미추적 파일이 있으면 실패한다. 변화가 없어도 실패하고, git 오류(종료 코드 2 이상)도 실패한다. 테스트는 이 단계를 임시 git 저장소에서 실제로 실행한다.
    • push 뒤 PR 생성이나 CI dispatch가 실패하면 마지막 정리 단계가 PR을 닫고 automation 브랜치를 지운다. job은 실패로 남는다(리뷰 F2).
  • .github/workflows/datapack-release-candidate-chain.yml(신규)
    • main push 중 후보 spec 또는 release request 경로가 바뀐 경우에만 돈다.
    • 그 사이 main의 후보가 다시 바뀌었으면(git diff 종료 1) dispatch하지 않고 notice를 남긴다. 새 push의 실행이 RC를 dispatch한다. git 오류(그 밖의 종료 코드)는 실패한다(리뷰 F1).
    • 그 밖에는 modeArgs를 만들어 datapack-release.yml을 mode=release-candidate, targetChannel=production으로 dispatch한다.
  • tools/ci/data-test-ownership.json: 새 테스트 등록(owner data870)

Scope

Included

  • 후보 갱신 CI 실행·자동 PR, 후보 병합 뒤 RC dispatch, 계약 테스트

Excluded

Ownership / dependencies

Contract & Compatibility

  • Source / API / schema contract: 변경 없음. 후보 생성 코드는 그대로 호출한다.
  • Artifact / provenance identity: 후보 산출물 형식 변경 없음. builderGitSha는 지금처럼 실행 시점 main HEAD다.
  • Backward compatibility: 수동 RC dispatch도 그대로 쓸 수 있다.
  • Migration or cutover: 없음

Version impact

  • no version change
  • datapack release only
  • route-map artifact change
  • data contract change
  • product gate JSON change
  • CI workflow·계약 테스트 change

Product gate impact

  • release/product-gates/** 영향 없음
  • 변경한 gate의 근거를 갱신했습니다.
  • 검증되지 않은 지원 범위 claim을 추가하거나 확대하지 않습니다.

Provenance impact

  • source inventory·geometry provenance manifest 영향 없음
  • 제공처·라이선스·갱신 시점·적용 범위를 갱신했습니다.
  • 공식 source로 확인되지 않은 값을 배포 artifact에 추가하지 않습니다.

Version decision

  • datapack version: 변경 없음(후보 sequence는 dispatch 입력)
  • data contract: 변경 없음
  • route-map artifact / product gate: 변경 없음
  • promotion request id: 해당 없음

Verification

Check Result / Evidence
Focused RED → GREEN 테스트를 먼저 썼다. 도구 부재로 ERR_MODULE_NOT_FOUND, 도구 구현 뒤에는 workflow 2개 부재로 2건 실패. 구현 후 node --test tools/ci/plan-datapack-release-chain.test.mjs: 11/11 통과
Affected integration refresh-nationwide-candidate·verify-release-request-binding 테스트 포함 32/32 통과. actionlint 새 workflow 2개: 오류 없음. data-test-discovery verify, anti-cheat guard, 문서 파편 검사 통과
Committed data 커밋된 seq126 후보 파일로 만든 modeArgs가 마지막 수동 RC(run 37109648483)의 modeArgs와 같은 키·approvalId다(테스트로 고정).
Required CI Data contracts 결과는 PR check에서 확인
Live provider / release Not required — reason: 외부 원천을 호출하지 않는다. 후보 갱신은 저장소 파일만 읽는다. 로컬 실측: 후보 시계 2026-10-03T05:50:00.000Z, seq 127 갱신이 7초에 끝나고 출력 목록 안의 8개 파일만 바뀌었다.
Security / data integrity 새 권한은 두 workflow의 actions: write(같은 저장소 workflow dispatch)뿐이다. dispatch 입력은 env로만 run에 넘긴다(테스트로 고정). publish·rollback 모드는 쓰지 않는다(테스트로 고정).

Not run

  • Check: 실제 dispatch run(후보 갱신 PR 생성, RC chain)
  • Reason: 병합 전에는 main에서만 도는 workflow를 실행할 수 없다. 실행 시점과 sequence는 메인 세션이 정한다.
  • Rerun owner / condition: 메인 세션이 병합 뒤 Nationwide Candidate Refresh를 한 번 dispatch하고, 생성 PR의 CI와 병합 뒤 RC dispatch를 확인한다.

Risk

  • Level: High
  • Main risk:
    • 오늘(2026-10-04) 커밋된 원천으로는 후보 갱신이 fan-in에서 실패한다(admission freshness mismatch for busan-transportation-accessibility). 접근성·지역 시간표 11개 원천의 inventory admission 창(수집+1일)이 이미 지났기 때문이다.
    • 이 workflow는 그 실패를 그대로 드러낸다(PR 없음). 원천 재수집 없이 후보를 만들 수는 없다.
  • Failure behavior: 후보 갱신이 실패하면 도구가 출력을 실행 전 바이트로 되돌리고 run이 실패한다. PR은 만들지 않는다. RC chain은 결속이 어긋나면 dispatch하지 않고 실패한다.
  • Candidate / admission / publication state on failure: 변경 없음
  • Fallback or degraded-success path introduced: No

Rollout / Recovery

Review focus

  • RC chain의 superseded 판정: 다른 commit이 main에 먼저 들어왔을 때 RC가 누락되지 않는지
  • 후보 갱신 workflow가 출력 목록 밖 경로를 커밋하지 못하는지

Checklist

  • 이슈 범위와 실제 diff가 일치합니다.
  • 관련 없는 변경이나 다른 owner의 surface를 포함하지 않았습니다.
  • 위험에 필요한 검증과 미실행 사유를 기록했습니다.
  • 실패·호환성·promotion·recovery 동작이 명확합니다.
  • current failure를 이전·stale·alternate 결과의 성공으로 바꾸지 않습니다.
  • GitHub PR Review 객체가 있는지 확인했습니다. CodeRabbit status check만으로는 리뷰 완료로 보지 않습니다.
  • CodeRabbit Review 객체가 없으면 지원되는 Codex CLI 폴백 Review를 단일 GitHub PR Review로 게시했습니다.
  • datapack 배포 영향이 있는 경우 release workflow 상태를 확인했습니다.

- nationwide-candidate-refresh.yml(workflow_dispatch, main 전용): 입력(releaseSequence·요청/승인 역할)을
  plan-datapack-release-chain로 먼저 검증하고(커밋된 후보보다 큰 sequence, 서로 다른 역할, 안전한 토큰),
  실행 시각을 후보 시계로 refresh-nationwide-candidate를 돌린다. NATIONWIDE_CANDIDATE_REFRESH_OUTPUTS 밖의
  경로가 바뀌거나 변화가 없으면 실패한다. 결과는 automation/927-… 브랜치의 draft PR로 올리고,
  GITHUB_TOKEN PR에는 pull_request CI가 붙지 않으므로 그 head에 ci.yml을 dispatch한다.
- datapack-release-candidate-chain.yml(push to main, 후보 spec·release request 경로): 저장소 파일로
  modeArgs를 만들어 datapack-release.yml을 mode=release-candidate로 dispatch한다. hub promotion이
  workflow_dispatch 후보 run만 받기 때문이다. release request가 spec에 결속되지 않거나 RC 증거 파일이
  없으면 dispatch 전에 실패한다. 후보가 main에서 다시 바뀌었으면 새 push 실행에 맡긴다.
- RED: 새 테스트가 도구·workflow 부재로 실패한 뒤 구현.

Refs #927
Refs #870
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 20603ff2-3853-4885-a503-4719025d1221
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@AquilaXk AquilaXk left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🎯 Linked issue & acceptance criteria audit
ID Acceptance criterion Status Evidence
AC-DISPATCH A main-only dispatch workflow takes the sequence and both roles PASS .github/workflows/nationwide-candidate-refresh.yml
AC-INPUTS Inputs are validated before the candidate command runs PASS tools/ci/plan-datapack-release-chain.mjs
AC-OUTPUTS Only the declared output paths may change and no change fails PASS .github/workflows/nationwide-candidate-refresh.yml
AC-PR-CI The result goes to an automation PR whose head receives a ci.yml dispatch PASS .github/workflows/nationwide-candidate-refresh.yml
AC-RC A candidate landing on main dispatches the RC with bound modeArgs PASS .github/workflows/datapack-release-candidate-chain.yml
🛡️ Adversarial audit evidence
  • Falsifiability verified: Yes (Probes on planner lines 31, 39, 45, 65 and 86 were killed by assertion failures.)
  • Hollow assertions detected: 0
  • Production backdoors detected: 0
  • Ground-truth sources verified:
    • datapack-release.yml workflow_dispatch input schema
    • hub datapack-promotion.yml event check
    • Issue 927 body acceptance criteria
  • Mutation testing evidence: probe lines 31, 39, 45, 65, 86 all killed
🤖 Prompt for all review comments with AI agents
Could we verify each finding against the current code and keep only those that
still apply? Could we make the smallest validated fix and briefly note why any
finding no longer applies?

Inline comments:
- `@.github/workflows/datapack-release-candidate-chain.yml:41-46`
  Could the step capture the exit code and treat only exit 1 as superseded, failing
  the job on any other code? A shell-level test with a bad ref would pin the failure.
- `@.github/workflows/nationwide-candidate-refresh.yml:75-82`
  Would a failure cleanup step that deletes the pushed branch when PR creation fails,
  or a final failure report naming the branch, keep the repository free of orphan
  branches?
- `@tools/ci/plan-datapack-release-chain.test.mjs:191-200`
  Could the expected key set and the fixed argument values from the manual run be
  pinned as literals, with the approvalId pattern asserted instead of echoed?
- `@tools/ci/plan-datapack-release-chain.test.mjs:202-240`
  Would a small test that runs the guard commands in a temporary git repo, with an
  extra tracked change and with no change, give the guard real coverage?

---

Outside diff comments:
None.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Aquila review of easysubway-data PR 928 using CLAUDE.md and the issue 927 body

Review profile: ASSERTIVE

Plan: Aquila fallback

Run ID: N/A (Aquila fallback)

Review source: Aquila CLI

Repository visibility: public GitHub repository

Trigger:
Manual discovery review requested by QA for the draft PR at current head

Base:
main@53f01fc5c4c5d2b7630c2c6dcdb79608200eaa1c

Head:
ci/nationwide-candidate-refresh-chain-927@3e028feed947a421870edd28796f26b14a6b9248

Command:
node aquila-review/scripts/post-review.mjs input.json payload-1.json --repo AquilaXk/easysubway-data --pr 928

📥 Commits

Reviewing files changed in this pull request between:

  • Base: 53f01fc5c4c5d2b7630c2c6dcdb79608200eaa1c
  • Head: 3e028feed947a421870edd28796f26b14a6b9248
📒 Files selected for processing (4)
  • .github/workflows/datapack-release-candidate-chain.yml
  • .github/workflows/nationwide-candidate-refresh.yml
  • tools/ci/plan-datapack-release-chain.mjs
  • tools/ci/plan-datapack-release-chain.test.mjs
📜 Review details

Comment thread .github/workflows/datapack-release-candidate-chain.yml Outdated
Comment thread .github/workflows/nationwide-candidate-refresh.yml
Comment thread tools/ci/plan-datapack-release-chain.test.mjs Outdated
Comment thread tools/ci/plan-datapack-release-chain.test.mjs
#928 리뷰 F1: if git diff --quiet ...; else 분기는 차이(종료 1)와 오류(잘못된 ref 등 128)를 모두 "superseded"로
처리해, 오류가 나도 run이 녹색으로 끝나고 RC가 dispatch되지 않았다.
종료 코드 0만 current, 1만 superseded로 두고 그 밖의 코드는 "candidate supersede check failed"로 실패한다.
RED: run 블록을 임시 git 저장소(bare origin 대신 로컬 origin)에서 실제 bash로 실행하는 테스트가 없는 sha에서 종료 0으로 실패.

Refs #927
#928 리뷰 F2: 브랜치 push 뒤 PR 생성이나 ci.yml dispatch가 실패하면 run id가 붙은 브랜치와 CI 없는 PR이 남았다.
- PR URL을 CANDIDATE_PR_URL로 남긴다.
- 마지막 단계 "Remove the candidate refresh branch after a later failure"(failure() && 브랜치가 있을 때)가
  PR이 있으면 gh pr close --delete-branch로 닫고, 없으면 git push origin --delete로 브랜치를 지운다.
  job은 실패로 남으므로 CI dispatch 실패도 run 실패로 드러난다.
RED: 정리 단계 부재로 새 테스트 실패. 테스트는 정리 run 블록을 임시 git 저장소와 가짜 gh로 실제 실행해 두 경우의 브랜치 삭제를 확인한다.

Refs #927
#928 리뷰 F3: 커밋된 파일 테스트가 releaseRequestId를 같은 파일의 approvalId와 비교해 순환 검증이었다.
- run 37109648483 "Parse modeArgs" 로그의 MODE_ARGS_INPUT 원문을 상수로 두고, seq126 후보 식별자로 만든 modeArgs가
  바이트 그대로 같은지 단언한다.
- 커밋된 파일 테스트는 releaseRequestId를 패턴(release-request-nationwide-candidate-YYYYMMDD-seqN)으로만 보고,
  나머지 고정 인자는 같은 원문 상수와 비교한다(이후 후보 PR이 release-request를 바꿔도 의미가 유지된다).
RED: allowGaps를 "true"로 바꾼 변이에서 새 테스트 2건이 실패(기존 키 순서 테스트는 이 변이를 잡지 못했다).

Refs #927
#928 리뷰 F4: 출력 범위·무변경 가드가 문자열 일치로만 테스트됐다.
- "Verify candidate refresh output scope" 단계로 분리한다(커밋·push 단계와 분리). bash 3.2에서도 도는 문법(mapfile 제거)으로 쓴다.
- 출력 목록을 못 읽으면 실패한다(명령 치환 실패가 set -e로 드러남).
- git diff --cached --quiet는 0(무변경)·1(변경)만 정상 분기로 두고 그 밖의 종료 코드는 실패한다(기존 && 형태는 오류를 변경으로 오인했다).
- 테스트는 이 run 블록을 임시 git 저장소에서 실행한다: 선언 출력만 변경(통과·스테이징 확인), 다른 추적 파일 변경, 미추적 파일, 무변경, 출력 목록 모듈 부재.
RED: 분리된 가드 단계 부재로 새 테스트 실패.

Refs #927
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

No description provided.

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@sonarqubecloud

sonarqubecloud Bot commented Oct 4, 2026

Copy link
Copy Markdown

@AquilaXk
AquilaXk merged commit 0971977 into main Oct 4, 2026
12 checks passed
@AquilaXk
AquilaXk deleted the ci/nationwide-candidate-refresh-chain-927 branch October 4, 2026 09:09
AquilaXk added a commit that referenced this pull request Oct 4, 2026
#928이 squash로 main에 병합되며 이 PR의 base가 main으로 바뀌고 DIRTY가 됐다. 이력을 다시 쓰지 않도록 rebase 대신 main을 합쳤다.
- #928 파일 4개(두 workflow, plan 도구·테스트): main 쪽 내용이 #928 마지막 커밋(9c6aeb7)과 같음을 확인하고, 그 위에 D3 변경을 더한 이 브랜치 내용을 유지했다.
- data-test-ownership.json: main의 항목에 scheduled-release-authority 테스트 한 줄을 더했다.
- documentation-fragment.json: main 기준으로 다시 생성했다(datapack-release.yml blob 갱신).

Refs #929
AquilaXk added a commit that referenced this pull request Oct 4, 2026
* [CI] 전국 후보 갱신을 CI 자동 PR로 만들고 후보 병합 시 release-candidate를 자동 실행

- nationwide-candidate-refresh.yml(workflow_dispatch, main 전용): 입력(releaseSequence·요청/승인 역할)을
  plan-datapack-release-chain로 먼저 검증하고(커밋된 후보보다 큰 sequence, 서로 다른 역할, 안전한 토큰),
  실행 시각을 후보 시계로 refresh-nationwide-candidate를 돌린다. NATIONWIDE_CANDIDATE_REFRESH_OUTPUTS 밖의
  경로가 바뀌거나 변화가 없으면 실패한다. 결과는 automation/927-… 브랜치의 draft PR로 올리고,
  GITHUB_TOKEN PR에는 pull_request CI가 붙지 않으므로 그 head에 ci.yml을 dispatch한다.
- datapack-release-candidate-chain.yml(push to main, 후보 spec·release request 경로): 저장소 파일로
  modeArgs를 만들어 datapack-release.yml을 mode=release-candidate로 dispatch한다. hub promotion이
  workflow_dispatch 후보 run만 받기 때문이다. release request가 spec에 결속되지 않거나 RC 증거 파일이
  없으면 dispatch 전에 실패한다. 후보가 main에서 다시 바뀌었으면 새 push 실행에 맡긴다.
- RED: 새 테스트가 도구·workflow 부재로 실패한 뒤 구현.

Refs #927
Refs #870

* [Fix] RC chain의 후보 변경 판정에서 git diff 오류를 건너뛰기로 처리하지 않고 실패

#928 리뷰 F1: if git diff --quiet ...; else 분기는 차이(종료 1)와 오류(잘못된 ref 등 128)를 모두 "superseded"로
처리해, 오류가 나도 run이 녹색으로 끝나고 RC가 dispatch되지 않았다.
종료 코드 0만 current, 1만 superseded로 두고 그 밖의 코드는 "candidate supersede check failed"로 실패한다.
RED: run 블록을 임시 git 저장소(bare origin 대신 로컬 origin)에서 실제 bash로 실행하는 테스트가 없는 sha에서 종료 0으로 실패.

Refs #927

* [Fix] 후보 갱신 push 뒤 단계가 실패하면 PR을 닫고 automation 브랜치를 지우기

#928 리뷰 F2: 브랜치 push 뒤 PR 생성이나 ci.yml dispatch가 실패하면 run id가 붙은 브랜치와 CI 없는 PR이 남았다.
- PR URL을 CANDIDATE_PR_URL로 남긴다.
- 마지막 단계 "Remove the candidate refresh branch after a later failure"(failure() && 브랜치가 있을 때)가
  PR이 있으면 gh pr close --delete-branch로 닫고, 없으면 git push origin --delete로 브랜치를 지운다.
  job은 실패로 남으므로 CI dispatch 실패도 run 실패로 드러난다.
RED: 정리 단계 부재로 새 테스트 실패. 테스트는 정리 run 블록을 임시 git 저장소와 가짜 gh로 실제 실행해 두 경우의 브랜치 삭제를 확인한다.

Refs #927

* [Test] RC modeArgs를 수동 RC run 37109648483의 실제 입력 원문으로 고정

#928 리뷰 F3: 커밋된 파일 테스트가 releaseRequestId를 같은 파일의 approvalId와 비교해 순환 검증이었다.
- run 37109648483 "Parse modeArgs" 로그의 MODE_ARGS_INPUT 원문을 상수로 두고, seq126 후보 식별자로 만든 modeArgs가
  바이트 그대로 같은지 단언한다.
- 커밋된 파일 테스트는 releaseRequestId를 패턴(release-request-nationwide-candidate-YYYYMMDD-seqN)으로만 보고,
  나머지 고정 인자는 같은 원문 상수와 비교한다(이후 후보 PR이 release-request를 바꿔도 의미가 유지된다).
RED: allowGaps를 "true"로 바꾼 변이에서 새 테스트 2건이 실패(기존 키 순서 테스트는 이 변이를 잡지 못했다).

Refs #927

* [Fix] 후보 출력 범위 가드를 별도 단계로 나누고 임시 git 저장소에서 실제 실행해 검증

#928 리뷰 F4: 출력 범위·무변경 가드가 문자열 일치로만 테스트됐다.
- "Verify candidate refresh output scope" 단계로 분리한다(커밋·push 단계와 분리). bash 3.2에서도 도는 문법(mapfile 제거)으로 쓴다.
- 출력 목록을 못 읽으면 실패한다(명령 치환 실패가 set -e로 드러남).
- git diff --cached --quiet는 0(무변경)·1(변경)만 정상 분기로 두고 그 밖의 종료 코드는 실패한다(기존 && 형태는 오류를 변경으로 오인했다).
- 테스트는 이 run 블록을 임시 git 저장소에서 실행한다: 선언 출력만 변경(통과·스테이징 확인), 다른 추적 파일 변경, 미추적 파일, 무변경, 출력 목록 모듈 부재.
RED: 분리된 가드 단계 부재로 새 테스트 실패.

Refs #927

* [Feat] 정기 실행 전용 2인 역할을 계약에 두고 release request에 후보 생성 run을 결속

QA 결정 D3(A)(2026-10-04): 정기 실행이 사람 승인 없이 후보를 만들 수 있게 하되, 그 사실과 근거를 계약으로 남긴다.
- lib/scheduled-release-authority: 고정 역할 requestedBy datapack-scheduled-refresh·approvedBy datapack-release-gates.
  - 이 두 라벨은 정확한 쌍으로만, schedule·workflow_run 이벤트에서만 쓸 수 있다. 사람 역할은 workflow_dispatch(또는 로컬)에서만 쓴다.
  - gateRun(repository, workflowPath, runId, runAttempt, event, headSha)은 Actions 기본 환경 변수로만 만들고,
    main의 nationwide-candidate-refresh.yml run이 아니면 거부한다. GitHub run 기록과 대조하는 함수도 둔다.
- verify-release-request-binding: 정기 역할이면 gateRun이 필수이고 그 event가 정기·체인이어야 한다. 사람 역할 request에
  정기 run을 붙일 수 없다. 이 검사는 build, 후보 갱신 결속 검증, RC chain, datapack-release의 binding 단계에 모두 걸린다.
- build-nationwide-candidate --gate-run, refresh-nationwide-candidate --gate-run: release request에 gateRun을 넣고
  결속 검증에서 이번 run과 같은지 본다. 정기 역할인데 --gate-run이 없으면 시작하지 않는다.
- plan-datapack-release-chain: candidate-refresh --event(정기·체인이면 사람 입력 없이 고정 역할과 커밋된 후보+1 sequence,
  사람 dispatch는 예약 라벨 거부), gate-run 하위 명령, release-candidate-mode-args --gate-run-record(결속된 run이
  main에서 성공한 그 run일 때만 modeArgs 생성).
- nationwide-candidate-refresh.yml: schedule(22:23 UTC)을 추가하되 vars.DATAPACK_SCHEDULED_CANDIDATE_REFRESH == 'true'일
  때만 돈다. 모든 run이 gateRun을 기록해 후보에 결속한다.
- datapack-release-candidate-chain.yml: gateRun이 있으면 gh api로 run 기록을 받아 대조한 뒤에만 RC를 dispatch한다.
- release-request 스키마에 선택 필드 gateRun을 추가했다.
RED: lib 모듈 부재, binding·build·refresh·plan 새 테스트가 변경 전 코드에서 실패.
실측: 저장소 사본에서 정기 역할과 gate-run 파일로 후보를 갱신(후보 시계 2026-10-03T05:50:00.000Z, seq 127)하면
release request에 gateRun이 들어가고 verify-release-request-binding CLI가 PASS다.

Refs #929
Refs #870

* [Fix] release request의 gateRun을 후보를 빌드한 커밋(builderGitSha)에 결속

#929 D3 보안 보강: gateRun 형식만 보면 다른 정기 run 기록을 가져다 붙인 request도 통과할 수 있다.
gateRun.headSha가 build spec builderGitSha와 다르면 결속 위반으로 실패한다. 후보 갱신 workflow는 checkout한
main 커밋에서 빌드하므로 정상 경로에서는 같다.
RED: 다른 커밋 headSha를 붙인 request가 위반 없이 통과해 새 단언이 실패.
실측: 저장소 사본에서 HEAD를 headSha로 둔 gate-run 파일로 정기 역할 후보 갱신이 성공하고 binding CLI가 PASS다.

Refs #929

* [Refactor] SonarCloud 지적 반영: gateRun 키 비교의 정렬 제거·경로 정규식 대신 split·인자 검사 함수 분리

S2871(비교 함수 없는 sort), S8786(역추적 정규식), S3776(parseRefreshNationwideCandidateArgs 복잡도)을 동작 변경 없이 고친다.
관련 테스트 65/65 통과.

Refs #929

* [Fix] 정기 전용 역할을 schedule 이벤트로만 한정

#931 리뷰 F3: 후보 갱신 workflow에는 workflow_run 트리거가 없는데 workflow_run을 정기 이벤트로 받아 허용 범위가
넓었다. SCHEDULED_ROLE_EVENTS를 ["schedule"]로 줄이고, planner도 workflow_run을 CANDIDATE_REFRESH_EVENT로 거부한다.
RED: workflow_run 거부 단언 5건이 변경 전 코드에서 실패.

Refs #929

* [Fix] gate run 기록 대조에 후보 시계 실행 창과 head_repository를 더해 지난 run 재사용 차단

#931 리뷰 F2: run 기록 대조가 run의 존재만 증명해, 공개된 지난 정기 성공 run의 id·커밋을 손으로 쓴 request에 붙일 수 있었다.
- 후보 시계(build spec publishedAt)가 그 run의 실행 창(run_started_at..updated_at) 안이어야 한다. 후보 시계는
  plan 단계의 실행 시각이라 정상 경로에서는 항상 창 안이다(QA 권장 (b)보다 좁은 ±0 창).
- head_repository.full_name도 같은 저장소여야 한다(QA 권장 (c)).
- 창 정보가 없거나 후보 시계 형식이 틀리면 실패한다.
RED: 창 밖 후보 시계·포크 head_repository·창 정보 누락이 변경 전 코드에서 통과해 새 테스트 2건 실패.

Refs #929

* [Fix] RC·production-publish 결속 단계에서 gateRun을 GitHub run 기록과 대조

#931 리뷰 F1(P1, 보안): gateRun 실재 대조가 RC chain에만 있어, 수동 RC나 production-publish는 손으로 쓴
gateRun(event schedule, 임의 runId)을 형식 검사만으로 통과시켰다.
- verify-release-request-binding CLI: request에 gateRun이 있으면 --gate-run-record가 필수이고
  gateRunRecordViolations(후보 시계 창 포함)를 통과해야 한다. gateRun 없는 request에 기록을 넘기면 실패한다.
- datapack-release.yml "Verify release request binding": production-publish와 release request를 쓰는 RC에서 돈다.
  request의 gateRun.runId로 gh api 기록을 받아 CLI에 넘긴다. 문서 파편을 갱신했다.
RED: 기록 없이 통과하던 정기 역할 request CLI 테스트와 binding 단계 계약 테스트가 실패.

Refs #929

* [Fix] RC chain이 검증한 커밋으로만 RC를 dispatch하고 dispatch된 run의 커밋을 확인

#931 리뷰 F1(P1, 보안): --ref main으로 dispatch해, 검증 뒤 main에 들어온 다른 커밋이 RC로 빌드될 수 있었다.
workflow_dispatch는 SHA를 받지 못하므로
- dispatch 직전 main 커밋이 검증한 GITHUB_SHA와 다르면 dispatch하지 않고 실패한다.
- dispatch 뒤 github-actions[bot]이 만든 RC run을 찾아 head_sha가 GITHUB_SHA인지 확인하고, 다르면 그 run을 취소하고 실패한다.
  run을 찾지 못해도 실패한다. 성공하면 rc_run_id를 출력한다.
RED: dispatch run 블록을 가짜 gh로 실행하는 테스트(커밋 일치·main 이동·다른 커밋 run)가 변경 전 workflow에서 실패.
실측: 같은 gh api 질의를 실제 저장소에 읽기로 실행해 run id·head_sha 출력 형식을 확인했다.

Refs #929
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge FIFO 병합 큐 대상 — 코디네이터가 순서대로 update-branch 후 auto-merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant