diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index ebeb485d69..cb3f10e0f6 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -258,6 +258,38 @@ jobs: } >>"$GITHUB_OUTPUT" printf 'Validated current live metadata for %s#%s: base=%s/%s head=%s/%s.\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "$live_base_ref" "$live_base_sha" "$live_head_ref" "$live_head_sha" + - name: Refuse private source on a public artifact destination + env: + SOURCE_REPOSITORY_PRIVATE: ${{ steps.validate.outputs.is_private }} + ARTIFACT_REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + # Artifact readers follow the receiving repository's current ACL. + # Do not use the dispatch event's potentially stale visibility. + case "$SOURCE_REPOSITORY_PRIVATE" in + false) ;; + true) + if ! [[ "$ARTIFACT_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo '::error::Receiving artifact repository identity is invalid.' + exit 1 + fi + if ! artifact_metadata="$(gh api "repos/${ARTIFACT_REPOSITORY}" 2>/dev/null)"; then + echo '::error::Receiving artifact repository visibility could not be read.' + exit 1 + fi + if ! jq -se --arg repository "$ARTIFACT_REPOSITORY" ' + length == 1 and (.[0] | + type == "object" and .full_name == $repository and + (.private | type == "boolean") and .private == true) + ' >/dev/null 2>&1 <<<"$artifact_metadata"; then + echo '::error::Private coverage source requires a currently private artifact destination; public or unknown destinations are refused.' + exit 1 + fi + ;; + *) echo '::error::Coverage source visibility is unknown.'; exit 1 ;; + esac + - name: Exchange OpenCode app token for target repository coverage reads id: coverage_read_app_token if: >- @@ -384,6 +416,39 @@ jobs: git -C "$COVERAGE_SOURCE_WORKDIR" status --short tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$COVERAGE_SOURCE_WORKDIR" . + - name: Recheck private artifact destination before upload + if: steps.validate.outputs.is_private == 'true' + env: + SOURCE_REPOSITORY_PRIVATE: ${{ steps.validate.outputs.is_private }} + ARTIFACT_REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + # Artifact readers follow the receiving repository's current ACL. + # Do not use the dispatch event's potentially stale visibility. + case "$SOURCE_REPOSITORY_PRIVATE" in + false) ;; + true) + if ! [[ "$ARTIFACT_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo '::error::Receiving artifact repository identity is invalid.' + exit 1 + fi + if ! artifact_metadata="$(gh api "repos/${ARTIFACT_REPOSITORY}" 2>/dev/null)"; then + echo '::error::Receiving artifact repository visibility could not be read.' + exit 1 + fi + if ! jq -se --arg repository "$ARTIFACT_REPOSITORY" ' + length == 1 and (.[0] | + type == "object" and .full_name == $repository and + (.private | type == "boolean") and .private == true) + ' >/dev/null 2>&1 <<<"$artifact_metadata"; then + echo '::error::Private coverage source requires a currently private artifact destination; public or unknown destinations are refused.' + exit 1 + fi + ;; + *) echo '::error::Coverage source visibility is unknown.'; exit 1 ;; + esac + - name: Upload materialized pull request merge tree if: steps.validate.outputs.is_private == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1079,12 +1144,26 @@ jobs: fi sudo rm -rf "$coverage_build_dir" + # Use the daemon's capacity, not the runner host's CPU count. + # Keep four CPUs as an upper limit without rejecting smaller guests. + if ! coverage_daemon_cpus="$(docker info --format '{{.NCPU}}')"; then + echo '::error::Coverage Docker daemon CPU capacity is unavailable.' + exit 1 + fi + if [[ ! "$coverage_daemon_cpus" =~ ^[1-9][0-9]*$ ]]; then + echo '::error::Coverage Docker daemon CPU capacity is invalid.' + exit 1 + fi + case "$coverage_daemon_cpus" in + 1|2|3) coverage_sandbox_cpus="$coverage_daemon_cpus" ;; + *) coverage_sandbox_cpus=4 ;; + esac sandbox_status=0 docker run --rm --init --network=none \ --name "opencode-coverage-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ --pids-limit 2048 \ --memory 14g \ - --cpus 4 \ + --cpus "$coverage_sandbox_cpus" \ --security-opt no-new-privileges:true \ --cap-drop ALL \ --cap-add CHOWN \ diff --git a/docs/doctoring/opencode-coverage-daemon-cpu-capacity.md b/docs/doctoring/opencode-coverage-daemon-cpu-capacity.md new file mode 100644 index 0000000000..2eda65469f --- /dev/null +++ b/docs/doctoring/opencode-coverage-daemon-cpu-capacity.md @@ -0,0 +1,20 @@ +# OpenCode coverage sandbox의 Docker CPU 용량 결속 + +## 실제 실패 + +공개 target `.github#2617@b3983b467c2d7f1440b5024acb6a3860eb3e043d`의 중앙 run `38443671498`, coverage job `115456508271`은 `orgmetra-ci-01` / group 13에서 실행됐다. 공개 exact source 확보와 tool image build는 성공했다. 이후 고정 `--cpus 4`가 CPU 2개인 Docker daemon에서 거부됐다. sandbox exit 125로 출력 파일이 생성되지 않았고 테스트·coverage 측정·모델 추론은 실행되지 않았다. diagnostic `COMMENTED` 리뷰는 정식 승인으로 인정하지 않는다. + +## 좁은 수리와 검증 + +Docker daemon의 기존 `docker info --format '{{.NCPU}}'` 결과를 양의 정수로 검증한다. sandbox CPU limit는 `min(4, NCPU)`로 설정한다. 조회 실패·빈 값·0·부호·소수·줄 삽입은 실행 전 거부한다. host의 `nproc`를 daemon 용량으로 대체하지 않는다. + +메모리 14g, PID limit, network none, capability set, mount, 출력 인증, cleanup 및 100% coverage 기준은 변경하지 않는다. CPU 수리를 메모리 적합성이나 실제 runner 실행의 성공으로 주장하지 않는다. 원래 실행은 고정 workflow source를 유지하므로 수리 소스를 소비하는 fresh 지원 경로가 별도로 필요하다. 새 dispatch나 reviewer identity 우회는 이 소스 수리에 포함하지 않는다. + +실제 YAML의 launch shell을 추출하고 synthetic Docker CLI만 치환하여 argv를 검증한다. 첫 harness는 동일 cleanup 문자열의 첫 위치를 잘못 선택해 실패했다. 위치를 바로잡은 후 원래 `4 != 2` assertion 실패를 관측했고, 최소 수리 후 CPU/거부 입력 15개가 통과했다. 이는 실제 Docker daemon이나 Actions 실행의 검증은 아니다. + +## 공식 문서 + +- Docker. (2026). *docker system info*. https://docs.docker.com/reference/cli/docker/system/info/ +- Docker. (2026). *Resource constraints*. https://docs.docker.com/engine/containers/resource_constraints/ + +부모의 실제 문서 조회는 첫 구현 및 15개 GREEN 이후인 2026-10-14에 수행했다. 이를 구현 전 조회로 소급하지 않는다. `--cpus`는 container CPU 사용의 상한이며 테스트 수나 정확성 기준을 바꾸는 옵션이 아니다. diff --git a/docs/doctoring/opencode-private-artifact-destination.md b/docs/doctoring/opencode-private-artifact-destination.md new file mode 100644 index 0000000000..e3a64770ff --- /dev/null +++ b/docs/doctoring/opencode-private-artifact-destination.md @@ -0,0 +1,22 @@ +# OpenCode private source의 artifact 수신 경계 + +## 요구와 근거 + +private PR source archive를 공개 review 저장소의 Actions artifact로 발행하지 않는다. GitHub 공식 안내는 workflow artifact의 다운로드 권한을 해당 저장소의 read access에 결속한다. private 원본 저장소의 ACL이나 `retention-days: 1`은 공개 수신 저장소의 다운로드 권한을 제한하지 않는다. + +- GitHub. (2026). *Downloading workflow artifacts*. https://docs.github.com/en/actions/how-tos/manage-workflow-runs/download-workflow-artifacts (확인: 2026-10-14). +- 근거 소스: `acb8b34f9e7b8649fa1478139fa21c2217bdfbc7`의 `.github/workflows/opencode-review-dispatch.yml`. + +## 좁은 수리 + +검증된 `steps.validate.outputs.is_private`와 현재 수신 저장소의 GitHub REST metadata를 비교한다. 이벤트 시점의 `github.event.repository.private`는 사용하지 않는다. private source는 token exchange·source fetch·archive 전에 수신 저장소를 조회하고, upload 직전에 같은 검사를 다시 실행한다. 응답의 `full_name`이 실제 수신 저장소와 같고 `private`이 Boolean true인 경우만 허용한다. API 실패·잘못된 JSON·여러 JSON 응답·누락·문자열 Boolean·public은 nonzero로 거부한다. public source는 이 추가 API를 호출하지 않는다. 새 허가 Boolean이나 정책 variable은 추가하지 않는다. + +public source의 credential-free, artifact-free 경로는 유지한다. private 수신 저장소의 기존 artifact 경로와 1일 보존값도 유지한다. 이 수리는 공개 중앙 저장소로의 private archive 전달을 금지하는 경계이며, 암호화 전송 구현이나 private 실행 활성화가 아니다. 실제 private 데이터 전송이나 과거 유출이 있었다고 주장하지 않는다. + +## 검증과 남은 조건 + +실제 YAML의 guard 위치·환경 binding을 검사하고 inline Bash를 합성 visibility 입력으로 실행한다. 정상 public 및 private destination을 별도 양성 대조로 유지한다. 원본 artifact-free helper의 실제 로컬 Git 회귀도 함께 실행한다. `tests/test_opencode_public_source_transport.py`는 기존 quality CI의 실행 목록에 포함되어 있다. + +GitHub REST `GET /repos/{owner}/{repo}`의 현재 metadata를 사용한다. 전송 전과 upload 직전 조회는 atomic ACL 보장이 아니며, 마지막 조회 후 또는 artifact 보존 중 저장소 공개 전환을 방지하지 않는다. private artifact 경로 활성화에는 별도의 수신 ACL·보존 정책이 필요하다. 현재 공개 중앙 저장소에서는 private source 전달이 거부된다. + +모델 비용·retention admission, 실제 source transport, 독립 App의 exact-head 리뷰, 원격 Actions 및 병합은 별도 gate다. private target의 나머지 경로를 이 guard만으로 모두 안전하다고 선언하지 않는다. guard를 우회하거나 정책 attestation을 조작해 실행하지 않는다. diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 234b23f914..4c46340904 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -3,10 +3,12 @@ import re import shutil import subprocess +import sys import textwrap from pathlib import Path import pytest +import yaml from scripts.ci.assert_opencode_reasoning_effort import strip_jsonc_comments @@ -16,6 +18,56 @@ def load_opencode_jsonc() -> dict: return json.loads(strip_jsonc_comments(Path("opencode.jsonc").read_text(encoding="utf-8"))) +@pytest.mark.parametrize("daemon_cpus, expected", [ + ("1", "1"), ("2", "2"), ("3", "3"), ("4", "4"), ("8", "4"), + ("999999999999999999999999999999999", "4"), + ("", None), ("0", None), ("-1", None), ("2.0", None), ("02", None), + ("true", None), ("2\n3", None), ("2\r", None), ("unavailable", None), +]) +def test_coverage_cpu_limit_fits_daemon_capacity(tmp_path, daemon_cpus, expected): + """실제 sandbox argv는 daemon 용량과 기존 상한 4를 넘지 않는다.""" + workflow = yaml.safe_load(Path(".github/workflows/opencode-review-dispatch.yml").read_text()) + steps = workflow["jobs"]["coverage-evidence"]["steps"] + script = next(step["run"] for step in steps if step.get("name") == "Measure test and docstring evidence") + start = script.rindex('sudo rm -rf "$coverage_build_dir"\n') + len('sudo rm -rf "$coverage_build_dir"\n') + end = script.index('sandbox_output="${sandbox_result_dir}/github-output"', start) + binary = tmp_path / "bin" + binary.mkdir() + (binary / "python3").symlink_to(sys.executable) + docker = binary / "docker" + docker.write_text( + f"#!{sys.executable}\nimport os,sys,json\nfrom pathlib import Path\n" + "if sys.argv[1]=='info':\n" + " assert sys.argv[2:]==['--format','{{.NCPU}}']\n" + " if os.environ['DAEMON_CPUS']=='unavailable':raise SystemExit(12)\n" + " print(os.environ['DAEMON_CPUS']);raise SystemExit(0)\n" + "Path(os.environ['ARGV_CAPTURE']).write_text(json.dumps(sys.argv[1:]))\n" + ) + docker.chmod(0o755) + capture = tmp_path / "argv.json" + result = subprocess.run( + ["/bin/bash", "-c", "set -euo pipefail\n" + script[start:end]], + env={"PATH": f"{binary}:/usr/bin:/bin", "HOME": str(tmp_path), "DAEMON_CPUS": daemon_cpus, + "ARGV_CAPTURE": str(capture), "GITHUB_RUN_ID": "1", "GITHUB_RUN_ATTEMPT": "1", + "GITHUB_WORKSPACE": str(tmp_path / "trusted"), "COVERAGE_SOURCE_WORKDIR": str(tmp_path / "source"), + "sandbox_result_dir": str(tmp_path / "out"), "measure_step_script": str(tmp_path / "measure.sh"), + "PR_BASE_SHA": "a" * 40, "PR_HEAD_SHA": "b" * 40, + "coverage_llvm_cov": "/llvm-cov", "coverage_llvm_profdata": "/llvm-profdata", + "coverage_tool_image": "synthetic:image"}, capture_output=True, text=True, check=False, timeout=15, + ) + if expected is None: + assert result.returncode == 1 + assert not capture.exists() + assert "CPU capacity is" in result.stdout + return + assert result.returncode == 0, result.stderr + argv = json.loads(capture.read_text()) + assert argv[argv.index("--cpus") + 1] == expected + assert argv[argv.index("--memory") + 1] == "14g" + assert "--network=none" in argv + assert "no-new-privileges:true" in argv + + def test_code_reviewer_subagent_contract_is_configured(): """Guard the read-only code-reviewer subagent contract.""" config = load_opencode_jsonc() diff --git a/tests/test_opencode_public_source_transport.py b/tests/test_opencode_public_source_transport.py index f5f4b40087..55cdf76bb8 100644 --- a/tests/test_opencode_public_source_transport.py +++ b/tests/test_opencode_public_source_transport.py @@ -1,7 +1,9 @@ """공개 exact-head 전달은 quota에 의존하지 않고 private 경로는 보존한다.""" from pathlib import Path +import json import os import subprocess +import sys import pytest import yaml @@ -29,6 +31,183 @@ def test_public_transfer_does_not_require_an_artifact_upload() -> None: assert download["with"]["name"] == upload["with"]["name"] +def test_private_source_refuses_public_artifact_destination_before_transport(tmp_path) -> None: + """비공개 PR 원문이 공개 리뷰 저장소의 artifact 저장소에 전달되지 않음을 검증한다.""" + jobs = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"] + steps = jobs["validate-pr-metadata"]["steps"] + names = [step.get("name") for step in steps] + guard = _step(jobs["validate-pr-metadata"], "Refuse private source on a public artifact destination") + assert names.index(guard["name"]) < names.index("Exchange OpenCode app token for target repository coverage reads") + assert names.index(guard["name"]) < names.index("Materialize pull request merge tree for coverage measurement") + assert names.index(guard["name"]) < names.index("Upload materialized pull request merge tree") + assert "if" not in guard + assert guard.get("continue-on-error", False) is False + assert guard["env"]["SOURCE_REPOSITORY_PRIVATE"] == "${{ steps.validate.outputs.is_private }}" + assert guard["env"]["ARTIFACT_REPOSITORY"] == "${{ github.repository }}" + assert guard["env"]["GH_TOKEN"] == "${{ github.token }}" + assert "ARTIFACT_REPOSITORY_PRIVATE" not in guard["env"] + for index, (source, destination, allowed) in enumerate(( + ("true", "false", False), ("true", "", False), ("true", "unknown", False), + ("true", "true", True), ("false", "false", True), ("false", "true", True), + ("true", '"true"', False), ("true", "null", False), + ("", "true", False), ("unknown", "true", False), ("True", "true", False), + )): + response = '{"full_name":"Fixture/review","private":' + (destination or "null") + '}' + result, _ = _run_receiving_guard(guard, tmp_path / str(index), source, response) + assert (result.returncode == 0) is allowed, (source, destination, result.stderr) + + +@pytest.mark.parametrize("destination, reaches_git", [("false", False), ("", False), ("true", True)]) +def test_private_destination_guard_stops_actual_materialization(destination, reaches_git, tmp_path): + """실제 guard와 materialize shell을 연결해 Git 진입 전 거부를 검증한다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + guard = _step(metadata, "Refuse private source on a public artifact destination")["run"] + materialize = _step(metadata, "Materialize pull request merge tree for coverage measurement")["run"] + binary = tmp_path / "bin" + binary.mkdir() + marker = tmp_path / "git-called" + git = binary / "git" + git.write_text( + f"#!{sys.executable}\nimport os\nfrom pathlib import Path\n" + "Path(os.environ['GIT_MARKER']).write_text('synthetic git boundary')\n" + "raise SystemExit(37)\n" + ) + git.chmod(0o755) + gh = binary / "gh" + gh.write_text( + f"#!{sys.executable}\nimport os\n" + "print(os.environ['API_RESPONSE'])\n" + ) + gh.chmod(0o755) + runner = tmp_path / "runner" + runner.mkdir() + result = subprocess.run( + ["/bin/bash", "-c", guard + "\n" + materialize], + capture_output=True, text=True, check=False, timeout=15, + env={"PATH": f"{binary}:/usr/bin:/bin", "HOME": str(tmp_path), + "SOURCE_REPOSITORY_PRIVATE": "true", "ARTIFACT_REPOSITORY": "Fixture/review", + "API_RESPONSE": '{"full_name":"Fixture/review","private":' + (destination or "null") + '}', + "RUNNER_TEMP": str(runner), "COVERAGE_SOURCE_WORKDIR": str(runner / "source"), + "COVERAGE_SOURCE_ARCHIVE": str(runner / "source.tar"), "GH_TOKEN": "synthetic-only", + "TARGET_REPOSITORY": "Fixture/private", "PR_NUMBER": "1", + "PR_BASE_SHA": "a" * 40, "PR_HEAD_SHA": "b" * 40, + "GITHUB_SERVER_URL": "https://fixture.invalid", "GIT_MARKER": str(marker)}, + ) + assert marker.exists() is reaches_git + assert result.returncode == (37 if reaches_git else 1) + assert not list(runner.iterdir()) + + +def _run_receiving_guard(guard, tmp_path, source, response, status=0, event_private="true"): + """실제 guard를 합성 GitHub metadata 경계와 실행하고 API argv를 보존한다.""" + binary = tmp_path / "bin" + binary.mkdir(parents=True) + calls = tmp_path / "api-calls" + gh = binary / "gh" + gh.write_text( + f"#!{sys.executable}\nimport json, os, sys\nfrom pathlib import Path\n" + "Path(os.environ['API_CALLS']).write_text(json.dumps(sys.argv[1:]))\n" + "print(os.environ['API_RESPONSE'])\n" + "raise SystemExit(int(os.environ['API_STATUS']))\n" + ) + gh.chmod(0o755) + result = subprocess.run( + ["/bin/bash", "-c", guard["run"]], capture_output=True, text=True, + check=False, timeout=15, + env={"PATH": f"{binary}:/usr/bin:/bin", "HOME": str(tmp_path), + "SOURCE_REPOSITORY_PRIVATE": source, "ARTIFACT_REPOSITORY_PRIVATE": event_private, + "ARTIFACT_REPOSITORY": "Fixture/review", "GH_TOKEN": "synthetic-only", + "API_CALLS": str(calls), "API_RESPONSE": response, "API_STATUS": str(status)}, + ) + return result, json.loads(calls.read_text()) if calls.exists() else None + + +def test_live_receiving_visibility_rejects_stale_private_event(tmp_path): + """이벤트가 private였어도 현재 수신 저장소가 public이면 전송 전에 거부한다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + guard = _step(metadata, "Refuse private source on a public artifact destination") + result, calls = _run_receiving_guard( + guard, tmp_path, "true", '{"full_name":"Fixture/review","private":false}' + ) + assert result.returncode != 0 + assert calls == ["api", "repos/Fixture/review"] + + +@pytest.mark.parametrize("source,response,status,allowed,query", [ + ("true", '{"full_name":"Fixture/review","private":true}', 0, True, True), + ("true", '{"full_name":"Fixture/review","private":false}', 0, False, True), + ("true", '{"full_name":"Fixture/review","private":"true"}', 0, False, True), + ("true", '{"full_name":"Fixture/review","private":null}', 0, False, True), + ("true", '{"full_name":"Fixture/review"}', 0, False, True), + ("true", '{"full_name":"Other/review","private":true}', 0, False, True), + ("true", '{"full_name":"Fixture/review","private":true}', 1, False, True), + ("true", 'invalid-json', 0, False, True), + ("true", '', 0, False, True), + ("true", '{"full_name":"Fixture/review","private":false}\n' + '{"full_name":"Fixture/review","private":true}', 0, False, True), + ("false", 'invalid-json', 1, True, False), + ("unknown", '{"full_name":"Fixture/review","private":true}', 0, False, False), +]) +def test_receiving_guard_uses_typed_live_metadata(source, response, status, allowed, query, tmp_path): + """현재 API의 저장소 identity와 Boolean을 확인하며 public 경로는 조회하지 않는다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + guard = _step(metadata, "Refuse private source on a public artifact destination") + result, calls = _run_receiving_guard(guard, tmp_path, source, response, status) + assert (result.returncode == 0) is allowed + assert calls == (["api", "repos/Fixture/review"] if query else None) + + +def test_private_upload_rechecks_live_receiving_visibility(tmp_path): + """archive 생성 뒤 업로드 직전에 동일한 fail-closed 조회를 다시 실행한다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + names = [step.get("name") for step in metadata["steps"]] + name = "Recheck private artifact destination before upload" + assert name in names + recheck = _step(metadata, name) + early = _step(metadata, "Refuse private source on a public artifact destination") + assert recheck["if"] == "steps.validate.outputs.is_private == 'true'" + assert recheck.get("continue-on-error", False) is False + assert names.index(name) + 1 == names.index("Upload materialized pull request merge tree") + assert recheck["env"] == early["env"] + assert recheck["run"] == early["run"] + result, calls = _run_receiving_guard( + recheck, tmp_path, "true", '{"full_name":"Fixture/review","private":false}' + ) + assert result.returncode != 0 + assert calls == ["api", "repos/Fixture/review"] + + +def test_private_to_public_transition_stops_upload_witness(tmp_path): + """전송 전 private 양성 뒤 public 전환을 재조회하여 upload witness 전에 거부한다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + early = _step(metadata, "Refuse private source on a public artifact destination") + late = _step(metadata, "Recheck private artifact destination before upload") + binary = tmp_path / "bin" + binary.mkdir() + calls = tmp_path / "calls" + upload = tmp_path / "upload-called" + gh = binary / "gh" + gh.write_text( + f"#!{sys.executable}\nimport json, os, sys\nfrom pathlib import Path\n" + "assert sys.argv[1:] == ['api', 'repos/Fixture/review']\n" + "calls = Path(os.environ['API_CALLS'])\n" + "n = int(calls.read_text()) if calls.exists() else 0\n" + "calls.write_text(str(n + 1))\n" + "print(json.dumps({'full_name': 'Fixture/review', 'private': n == 0}))\n" + ) + gh.chmod(0o755) + result = subprocess.run( + ["/bin/bash", "-c", early["run"] + "\n" + late["run"] + '\ntouch "$UPLOAD_MARKER"'], + capture_output=True, text=True, check=False, timeout=15, + env={"PATH": f"{binary}:/usr/bin:/bin", "HOME": str(tmp_path), + "SOURCE_REPOSITORY_PRIVATE": "true", "ARTIFACT_REPOSITORY": "Fixture/review", + "GH_TOKEN": "synthetic-only", "API_CALLS": str(calls), "UPLOAD_MARKER": str(upload)}, + ) + assert result.returncode == 1 + assert calls.read_text() == "2" + assert not upload.exists() + + def test_public_source_is_reconstructed_in_the_credential_free_coverage_job() -> None: """공개 경로도 검증한 base/head와 격리된 기존 destination에 결속한다.""" jobs = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"] diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 625ddaa21b..da69740c25 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -17,7 +17,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "ebeb485d690cfa7e6c368befb21ce73f74cb2a4c" +REVIEW_DISPATCH_BLOB_SHA = "cb3f10e0f616e0ae443206a22d379173fe5056b1" def _workflow_text(path: Path) -> str: