From 2320129a0c5be139ae913e1f271e04308d96bb22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:37:38 +0900 Subject: [PATCH 1/4] test(ci): preserve admission outcomes in the bootstrap runner --- .../test_opencode_required_rerun_capacity.py | 24 ++++++++++++ ...st_opencode_required_verdict_regression.py | 38 +++++++++++++------ 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/tests/test_opencode_required_rerun_capacity.py b/tests/test_opencode_required_rerun_capacity.py index 431d3a8bc2..e1c9b2c290 100644 --- a/tests/test_opencode_required_rerun_capacity.py +++ b/tests/test_opencode_required_rerun_capacity.py @@ -12,6 +12,30 @@ DISPATCH = Path(".github/workflows/opencode-review-dispatch.yml") +def test_live_head_admission_reuses_the_trusted_bootstrap_runner() -> None: + """Keep admission and policy order without a second runner or lost contexts.""" + required = REQUIRED.read_text(encoding="utf-8") + bootstrap = required.split(" required-workflow-bootstrap:\n", 1)[1].split( + "\n coverage-source-tree:\n", 1 + )[0] + assert "\n admit-current-head:\n" not in required + assert required.count("runs-on:") == 5 + assert "admitted: ${{ steps.live_head.outputs.admitted }}" in bootstrap + assert bootstrap.index("Enforce Cloudflare Pingora edge policy") < bootstrap.index( + "Admit only the exact live OpenCode head" + ) + assert "GITHUB_ENV" not in bootstrap + assert "GITHUB_PATH" not in bootstrap + assert "actions/checkout" not in bootstrap + assert "${{ secrets." not in bootstrap + assert required.count("needs: [required-workflow-bootstrap]") == 3 + assert required.count( + "if: needs.required-workflow-bootstrap.outputs.admitted == 'true'" + ) == 3 + assert " name: coverage-source-tree\n" in required + assert " name: coverage-evidence\n" in required + + def test_required_job_releases_runner_until_exact_run_wakeup() -> None: required = REQUIRED.read_text(encoding="utf-8") target = required.split(" opencode-review-target:\n", 1)[1].split( diff --git a/tests/test_opencode_required_verdict_regression.py b/tests/test_opencode_required_verdict_regression.py index f29b97a663..9c265c9852 100644 --- a/tests/test_opencode_required_verdict_regression.py +++ b/tests/test_opencode_required_verdict_regression.py @@ -48,11 +48,25 @@ def admission_script() -> str: return textwrap.dedent(step.split(" run: |\n", 1)[1].split("\n\n coverage-source-tree:", 1)[0]) -def test_stale_opencode_event_never_reaches_review_concurrency(tmp_path: Path) -> None: - """A delayed old synchronize event is retired by live-head admission.""" +@pytest.mark.parametrize( + ("live_head", "live_state", "event_action", "api_status", "admitted"), + ( + (HEAD, "open", "synchronize", 0, True), + ("b" * 40, "open", "synchronize", 0, False), + (HEAD, "closed", "synchronize", 0, False), + (HEAD, "closed", "closed", 0, True), + (HEAD, "open", "closed", 0, False), + (HEAD, "open", "synchronize", 23, False), + ), +) +def test_opencode_admission_preserves_live_state_and_api_failure( + tmp_path: Path, live_head: str, live_state: str, event_action: str, + api_status: int, admitted: bool, +) -> None: + """Run the production gate; stale evidence retires while API errors fail.""" fake_gh = tmp_path / "gh" fake_gh.write_text( - "#!/usr/bin/env bash\nprintf '%s' '{\"head\":{\"sha\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"},\"state\":\"open\"}'\n", + "#!/bin/sh\nprintf '%s' \"$LIVE_PR_JSON\"\nexit \"$API_STATUS\"\n", encoding="utf-8", ) fake_gh.chmod(0o755) @@ -60,22 +74,24 @@ def test_stale_opencode_event_never_reaches_review_concurrency(tmp_path: Path) - result = subprocess.run( [shutil.which("bash") or "/bin/bash", "-c", admission_script()], env={ - **os.environ, - "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "PATH": f"{tmp_path}:/opt/homebrew/bin:/usr/bin:/bin", + "LIVE_PR_JSON": json.dumps({"head": {"sha": live_head}, "state": live_state}), + "API_STATUS": str(api_status), "GH_TOKEN": "synthetic-token", "GITHUB_OUTPUT": str(output), "TARGET_REPOSITORY": "ContextualWisdomLab/example", "PR_NUMBER": "7", "EXPECTED_HEAD_SHA": HEAD, - "EXPECTED_ACTION": "synchronize", + "EXPECTED_ACTION": event_action, }, capture_output=True, text=True, check=False, ) - assert result.returncode == 0, result.stderr - assert output.read_text(encoding="utf-8").splitlines() == ["admitted=false"] - assert "retired a stale event" in result.stdout + assert result.returncode == api_status, result.stderr + expected_output = ["admitted=false"] + (["admitted=true"] if admitted else []) + assert output.read_text(encoding="utf-8").splitlines() == expected_output + assert ("retired a stale event" in result.stdout) is (not admitted and api_status == 0) def test_opencode_dispatch_uses_the_same_target_repo_pr_group() -> None: @@ -639,9 +655,7 @@ def test_opencode_review_concurrency_group_is_workflow_level_repo_and_pr() -> No assert "github.event.pull_request.number || github.run_id" in concurrency_block assert "cancel-in-progress: true" in concurrency_block assert " concurrency:" not in target_job.split(" permissions:", 1)[0] - admission = workflow.split("\n admit-current-head:\n", 1)[1].split( - "\n coverage-source-tree:", 1 - )[0] + admission = admission_script() assert "live_head" in admission assert "live_state" in admission assert 'echo "admitted=false"' in admission From adc756a6e9e7fd57e3176f7d537c260c95b44671 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:38:46 +0900 Subject: [PATCH 2/4] fix(ci): fold live-head admission into trusted OpenCode bootstrap --- .github/workflows/opencode-review.yml | 32 +++++++------------ ...st_opencode_required_verdict_regression.py | 5 ++- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 19ea58003f..b79b807490 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -34,6 +34,8 @@ jobs: required-workflow-bootstrap: name: required-workflow-bootstrap runs-on: ubuntu-24.04 + outputs: + admitted: ${{ steps.live_head.outputs.admitted }} steps: - name: Materialize the required review workflow run: >- @@ -236,19 +238,10 @@ jobs: --event-action "$EVENT_ACTION" \ --api-url "https://api.github.com" - admit-current-head: - name: admit-current-head - needs: [required-workflow-bootstrap] - runs-on: ubuntu-24.04 - timeout-minutes: 5 - outputs: - admitted: ${{ steps.live_head.outputs.admitted }} - permissions: - contents: read - pull-requests: read - steps: + # Preserve policy-before-admission ordering without another runner queue. - name: Admit only the exact live OpenCode head id: live_head + timeout-minutes: 5 env: GH_TOKEN: ${{ github.token }} TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} @@ -278,8 +271,8 @@ jobs: coverage-source-tree: name: coverage-source-tree - needs: [required-workflow-bootstrap, admit-current-head] - if: needs.admit-current-head.outputs.admitted == 'true' + needs: [required-workflow-bootstrap] + if: needs.required-workflow-bootstrap.outputs.admitted == 'true' runs-on: ubuntu-24.04 steps: - run: >- @@ -295,11 +288,10 @@ jobs: # naruon#1528 (run 33581213805): coverage-source-tree waited 9h40m to run for # 4s, then coverage-evidence waited a further 13h01m to run for 5s, holding # the actual review behind ~22h41m of pure queueing. Depending on - # `admit-current-head` directly lets the two run in parallel. The `if:` below - # restates the admission gate this job previously inherited transitively - # through coverage-source-tree, so an unadmitted head still skips it. - needs: [required-workflow-bootstrap, admit-current-head] - if: needs.admit-current-head.outputs.admitted == 'true' + # the bootstrap's admitted output lets the two run in parallel while still + # skipping an event that failed exact-live-head admission. + needs: [required-workflow-bootstrap] + if: needs.required-workflow-bootstrap.outputs.admitted == 'true' runs-on: ubuntu-24.04 steps: - run: >- @@ -317,8 +309,8 @@ jobs: # created until its `needs:` finish, so this link cost a further 12h13m of # queue wait on naruon#1528 (run 33581213805). Admission is still enforced # directly by this job's own `if:` below, not inherited through that edge. - needs: [admit-current-head] - if: needs.admit-current-head.outputs.admitted == 'true' + needs: [required-workflow-bootstrap] + if: needs.required-workflow-bootstrap.outputs.admitted == 'true' runs-on: ubuntu-24.04 permissions: contents: read diff --git a/tests/test_opencode_required_verdict_regression.py b/tests/test_opencode_required_verdict_regression.py index 9c265c9852..ab598841a4 100644 --- a/tests/test_opencode_required_verdict_regression.py +++ b/tests/test_opencode_required_verdict_regression.py @@ -70,11 +70,14 @@ def test_opencode_admission_preserves_live_state_and_api_failure( encoding="utf-8", ) fake_gh.chmod(0o755) + jq_executable = shutil.which("jq") + assert jq_executable is not None, "jq is required to execute the production gate" + (tmp_path / "jq").symlink_to(jq_executable) output = tmp_path / "github-output" result = subprocess.run( [shutil.which("bash") or "/bin/bash", "-c", admission_script()], env={ - "PATH": f"{tmp_path}:/opt/homebrew/bin:/usr/bin:/bin", + "PATH": str(tmp_path), "LIVE_PR_JSON": json.dumps({"head": {"sha": live_head}, "state": live_state}), "API_STATUS": str(api_status), "GH_TOKEN": "synthetic-token", From 08866a699917940b4be4acfc757c6946b8aabcec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:53:26 +0900 Subject: [PATCH 3/4] =?UTF-8?q?docs(ci):=20OpenCode=20=EC=A0=91=EC=88=98?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9=EC=9D=98=20=EB=B3=B4=ED=98=B8=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=EA=B3=BC=20=EC=B8=A1=EC=A0=95=20=EA=B7=BC=EA=B1=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 접수 shell과 coverage 검사 이름을 보존하고, metadata step budget을 기존 회귀 검사에 고정한다. 새 workflow나 권한을 추가하지 않는다. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .github/workflows/opencode-review.yml | 2 +- CHANGELOG.md | 8 +++ ...ncode-bootstrap-admission-consolidation.md | 70 +++++++++++++++++++ .../test_opencode_required_rerun_capacity.py | 1 + ...st_opencode_required_verdict_regression.py | 2 +- 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 docs/doctoring/opencode-bootstrap-admission-consolidation.md diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index b79b807490..94f6437160 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -17,7 +17,7 @@ on: types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] concurrency: - # Coalesce before runner admission. The live-head job and scheduler still + # Coalesce before runner admission. The live-head step and scheduler still # reject or replace a delayed stale event after native queue cancellation. group: >- required-opencode-review-${{ diff --git a/CHANGELOG.md b/CHANGELOG.md index 75a4109c9d..70d9889959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### OpenCode 접수 전용 runner 통합 + +- Required OpenCode의 live-head 접수를 기존 trusted bootstrap의 마지막 step으로 + 옮겨 유효 PR 진입 경로의 job 수를 5개에서 4개로 줄인다. 접수 순서·5분 metadata + budget·필수 coverage context·동일 PR 구형 실행 취소는 보존한다. API 오류는 + bootstrap 실패로 남으며 모델 실행 시간을 제한하지 않는다. 보호 설정 조사와 + 실제 GitHub 실행 검증 경계는 [검증 기록](docs/doctoring/opencode-bootstrap-admission-consolidation.md)에 남긴다. + ### Strix gate names the sandbox bootstrap failure and retries it once - `scripts/ci/strix_quick_gate.sh` gives the Caido sandbox bootstrap race (`loginAsGuest failed after 10 attempts` on `127.0.0.1:`, upstream usestrix/strix#1036/#1037/#1056) its own bounded same-model retry budget, `STRIX_SANDBOX_BOOTSTRAP_RETRIES` (default 1), drawn on top of `STRIX_TRANSIENT_RETRY_PER_MODEL`. That budget is 0 in production because the gateway owns model failover, so the documented sandbox retry never ran: `argos` Strix run 34013128112 (2026-09-06) shows one attempt, `Docker image ready`, the proxy never reachable, Strix exiting after 240 s -- while the sidecar reported four ready and four deferred routes that were never called. The budget is charged in the same branch that grants the attempt, so a log matching the sandbox class together with a gateway class cannot extend the loop without charging it (caught by adversarial review of the first draft). The primary-scan verdict for that class now reads `STRIX_PROVIDER_UNAVAILABLE: STRIX_SANDBOX_UNAVAILABLE: the last Strix attempt ended in the sandbox bootstrap (...) after N sandbox-specific same-model retries (budget B); this verdict names Strix's sandbox, not the LLM gateway.` instead of `orchestrator/free exhausted`, stating only what the gate observed; the leading token is unchanged so the workflow's finding-free classification and its tests are untouched, and the second token lets the review census split sandbox outages from gateway ones (two of six recent Strix artifacts were this class). Refs #1948. diff --git a/docs/doctoring/opencode-bootstrap-admission-consolidation.md b/docs/doctoring/opencode-bootstrap-admission-consolidation.md new file mode 100644 index 0000000000..e06116dc21 --- /dev/null +++ b/docs/doctoring/opencode-bootstrap-admission-consolidation.md @@ -0,0 +1,70 @@ +# OpenCode 접수 runner 통합 + +- 상태: Proposed. 보호 병합·실제 runner 검증 전. +- 기준: `main@43024633eba9d96b0456970391360da5a171fbda`, 2026-09-06. +- 범위: `.github/workflows/opencode-review.yml`와 기존 접수 회귀 테스트. + +## 문제와 선택 + +기존 경로는 trusted bootstrap 뒤에 live-head 접수만 수행하는 runner를 하나 더 +배정했다. 접수 shell을 bootstrap의 Pingora 정책 검사 뒤로 옮기고, step 출력을 +job 출력으로 전달한다. 세 후속 job은 bootstrap 성공과 접수 결과를 직접 요구한다. +새 workflow·helper·의존성·API 호출·polling을 추가하지 않는다. + +접수 shell은 기준 커밋과 동일하다. SHA-256은 +`de6d073fec78d249c92c153f1c5235bc8c6858984f43be3b106d2d596aeb5c3a`다. +기존 접수 job의 5분 상한은 해당 metadata step에 보존한다. 모델 timeout이 아니다. +오래된 이벤트는 성공한 `admitted=false`로 종료한다. API 오류와 잘못된 이벤트 +입력은 이제 별도 접수 job 대신 필수 bootstrap을 실패시키며 후속 작업을 막는다. + +bootstrap의 기존 read 권한과 OIDC 권한을 유지한다. 옮긴 step은 같은 신뢰 작업 +경계를 공유하지만 OIDC를 요청하지 않으며 PR 코드를 checkout하거나 실행하지 않는다. +별도 cleanup job의 Actions write 권한과 workflow concurrency는 그대로 둔다. + +## 제거하지 않은 작업과 근거 + +2026-09-06 읽기 전용 GitHub API 조사에서 활성 저장소 75개, 보호 브랜치 409개, +고유 ruleset 34개를 확인했다. 기존 두 접수 표시명 `admit-current-head`와 +`Admit current pull request head`를 요구하는 status context는 없었다. +단순 출력만 하는 `coverage-source-tree`도 Naruon·linux-cluster-ops의 `develop` +보호 설정이 요구한다. 따라서 두 coverage context와 실제 dispatch coverage 작업은 +삭제하지 않았다. 권한이 다른 cleanup 통합도 이 변경에서 제외했다. + +조사 경로는 `repos/{repo}/branches?protected=true`, 각 저장소의 +`rulesets?includes_parents=true`, 고유 ruleset 상세이며 모든 페이지를 포함했다. +원시 조사 파일 SHA-256은 +`db783b2bcfa48ad9354ba075aad8e4d42fd379ac7f3e3b23b08519f1a88104ff`다. +이 조사는 시점 증거다. 병합 전 보호 설정을 다시 확인하고 바뀐 필수 context가 +있으면 삭제 후보를 재평가한다. 설정을 지우거나 gate를 완화하지 않는다. + +## 측정과 검증 + +| 선언된 실행 경로 | 기준 | 후보 | 감소율 | +|---|---:|---:|---:| +| 유효 opened/ready PR job | 5 | 4 | 20% | +| synchronize job, 별도 cleanup 포함 | 6 | 5 | 16.7% | +| review 진입까지 직렬 runner 배정 단계 | 3 | 2 | 33.3% | + +이는 job 구조의 감소량이며 조직 전체 runner 점유·대기시간·41개 요구의 완료율이 +아니다. 기준의 관련 테스트 48개는 통과했다. 접수 계약을 먼저 추가한 커밋 +`2320129a0c5be139ae913e1f271e04308d96bb22`에서 1 failed/52 passed를 확인한 뒤 통합 구현을 적용했다. +실제 접수 shell을 격리된 가짜 GitHub 응답으로 실행해 현재/구형 head, 열린/닫힌 +PR, 닫힘 이벤트, API 실패 여섯 경우를 검증한다. 운영 환경 변수는 상속하지 않는다. + +```sh +python -m pytest -q -W error tests/test_opencode_required_rerun_capacity.py tests/test_opencode_required_verdict_regression.py tests/test_pingora_edge_workflow_contract.py tests/test_opencode_agent_contract.py tests/test_required_workflow_queue_contract.py tests/test_opencode_coverage_identity.py tests/test_opencode_coverage_publication_regression.py +actionlint .github/workflows/opencode-review.yml +CI=true GITHUB_ACTIONS=true python -m pytest -q -W error tests --cov --cov-branch --cov-report=term-missing +``` + +각 명령은 저장소에서 실행한다. 정확한 후보 SHA의 결과·종료 코드·실패 분모는 +PR에 기록한다. 기존 HTTP 응답 정리 결함은 선행 #1879에서 해결하며 이 후보에 +runtime 코드를 복제하거나 warning을 숨기지 않는다. 선행 보호 병합 뒤 새 base를 +일반 merge로 반영하고 전체 suite를 재검증한다. 로컬 테스트는 GitHub의 output 전달, +step budget, `needs` 실행 증거가 아니므로 병합 뒤 해당 SHA의 실제 run도 확인한다. +회귀 시 이 통합 변경 전체를 일반 revert해 접수 job·세 의존 경로를 함께 복원한다. + +## 근거 + +- GitHub. (n.d.-a). [*Passing information between jobs*](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/pass-job-outputs). Retrieved September 6, 2026. +- GitHub. (n.d.-b). [*Workflow syntax for GitHub Actions*](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idneeds). Retrieved September 6, 2026. `needs` 실패 전파와 step `timeout-minutes` 계약. diff --git a/tests/test_opencode_required_rerun_capacity.py b/tests/test_opencode_required_rerun_capacity.py index e1c9b2c290..80e64bb2a2 100644 --- a/tests/test_opencode_required_rerun_capacity.py +++ b/tests/test_opencode_required_rerun_capacity.py @@ -21,6 +21,7 @@ def test_live_head_admission_reuses_the_trusted_bootstrap_runner() -> None: assert "\n admit-current-head:\n" not in required assert required.count("runs-on:") == 5 assert "admitted: ${{ steps.live_head.outputs.admitted }}" in bootstrap + assert "id: live_head\n timeout-minutes: 5\n" in bootstrap assert bootstrap.index("Enforce Cloudflare Pingora edge policy") < bootstrap.index( "Admit only the exact live OpenCode head" ) diff --git a/tests/test_opencode_required_verdict_regression.py b/tests/test_opencode_required_verdict_regression.py index ab598841a4..ca1e280d9b 100644 --- a/tests/test_opencode_required_verdict_regression.py +++ b/tests/test_opencode_required_verdict_regression.py @@ -42,7 +42,7 @@ def fail_closed_script() -> str: def admission_script() -> str: - """Extract the exact-head admission shell that precedes concurrency.""" + """Extract the exact-head admission shell after the trusted policy steps.""" workflow = WORKFLOW.read_text(encoding="utf-8") step = workflow.split(" - name: Admit only the exact live OpenCode head\n", 1)[1] return textwrap.dedent(step.split(" run: |\n", 1)[1].split("\n\n coverage-source-tree:", 1)[0]) From 1c6241334daf641a885ef15da9516a558a787f76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:58:32 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs(ci):=20=EC=A0=91=EC=88=98=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=EC=9D=98=20OIDC=20=EA=B2=BD=EA=B3=84=EC=99=80=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=EC=8B=A4=ED=8C=A8=EB=A5=BC=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit read-only 전용 job과 OIDC-capable bootstrap의 차이를 기록한다. 격리된 macOS 환경에서 기준과 후보의 동일한 12개 실패를 재현했으며 전체 통과로 보고하지 않는다. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../opencode-bootstrap-admission-consolidation.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/opencode-bootstrap-admission-consolidation.md b/docs/doctoring/opencode-bootstrap-admission-consolidation.md index e06116dc21..edfc000ecd 100644 --- a/docs/doctoring/opencode-bootstrap-admission-consolidation.md +++ b/docs/doctoring/opencode-bootstrap-admission-consolidation.md @@ -17,8 +17,10 @@ job 출력으로 전달한다. 세 후속 job은 bootstrap 성공과 접수 결 오래된 이벤트는 성공한 `admitted=false`로 종료한다. API 오류와 잘못된 이벤트 입력은 이제 별도 접수 job 대신 필수 bootstrap을 실패시키며 후속 작업을 막는다. -bootstrap의 기존 read 권한과 OIDC 권한을 유지한다. 옮긴 step은 같은 신뢰 작업 -경계를 공유하지만 OIDC를 요청하지 않으며 PR 코드를 checkout하거나 실행하지 않는다. +bootstrap의 기존 read 권한과 OIDC 권한을 유지한다. 이전 접수 전용 job에는 +OIDC 권한이 없었지만, 옮긴 step은 이제 `id-token: write`를 상속하는 job 안에서 +실행된다. 접수 step 자체는 OIDC 토큰을 요청하지 않으며 PR 코드를 checkout하거나 +실행하지 않는다. 이 권한 경계 차이는 runner 통합의 명시적인 검토 대상이다. 별도 cleanup job의 Actions write 권한과 workflow concurrency는 그대로 둔다. ## 제거하지 않은 작업과 근거 @@ -58,7 +60,9 @@ CI=true GITHUB_ACTIONS=true python -m pytest -q -W error tests --cov --cov-branc ``` 각 명령은 저장소에서 실행한다. 정확한 후보 SHA의 결과·종료 코드·실패 분모는 -PR에 기록한다. 기존 HTTP 응답 정리 결함은 선행 #1879에서 해결하며 이 후보에 +PR에 기록한다. 격리된 macOS PATH의 전체 검증은 기준·후보 모두 같은 12개 실패를 +재현했다. 11개는 HTTP 응답 정리, 1개는 기존 token-file 특수 권한 비트 검사다. +전자는 선행 #1879에서 해결하며 후자는 별도 원인 조사 대상으로 남긴다. 이 후보에 runtime 코드를 복제하거나 warning을 숨기지 않는다. 선행 보호 병합 뒤 새 base를 일반 merge로 반영하고 전체 suite를 재검증한다. 로컬 테스트는 GitHub의 output 전달, step budget, `needs` 실행 증거가 아니므로 병합 뒤 해당 SHA의 실제 run도 확인한다.