From a1c7958aed25b31723e8aeb0bef2cddf01a5518a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 30 Sep 2026 04:44:06 +0900 Subject: [PATCH 1/3] fix(coverage): install the base-pinned Rust release in the coverage image Debian's rustc 1.85 is older than fast-mlsirm's pinned 1.97.1 (its graph needs >=1.90), so the offline maturin build failed, _core never imported, and every fast-mlsirm coverage run fell below its 100% gate. When the validated base commit pins an exact 1.x.y release, the trusted image installs it from a SHA-256-verified rustup-init (minimal profile + llvm-tools-preview) and binds Rust coverage to that release's LLVM tools. Unpinned repositories keep the Debian image; a failed pinned layer rebuilds the previous image. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PtgtJk1WjqieqvLDb1w8uW --- .../agent-review-runtime-quality-ci.yml | 8 +- .../workflows/opencode-review-dispatch.yml | 65 +++++++++++++-- .../20260930-coverage-base-pinned-rust.md | 10 +++ ...opencode-rust-coverage-runtime-boundary.md | 30 ++++++- scripts/ci/resolve_base_rust_toolchain.py | 82 ++++++++++++++++++ ...encode_rust_coverage_toolchain_contract.py | 54 ++++++++++-- ...t_pr_review_autofix_nvidia_nim_contract.py | 2 +- tests/test_resolve_base_rust_toolchain.py | 83 +++++++++++++++++++ 8 files changed, 315 insertions(+), 19 deletions(-) create mode 100644 CHANGELOG.d/20260930-coverage-base-pinned-rust.md create mode 100755 scripts/ci/resolve_base_rust_toolchain.py create mode 100644 tests/test_resolve_base_rust_toolchain.py diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index 2b4589a46e..f3b9200414 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -21,6 +21,8 @@ on: - ".github/workflows/opencode-review-dispatch.yml" - "scripts/ci/ensure_rust_llvm19.sh" - "tests/test_opencode_rust_coverage_toolchain_contract.py" + - "scripts/ci/resolve_base_rust_toolchain.py" + - "tests/test_resolve_base_rust_toolchain.py" - "scripts/ci/materialize_base_javascript_packages.py" - "tests/test_javascript_materializer_docstrings.py" - "tests/test_pr_review_autofix_nvidia_nim_contract.py" @@ -204,6 +206,8 @@ jobs: .github/workflows/opencode-review-dispatch.yml|\ scripts/ci/ensure_rust_llvm19.sh|\ tests/test_opencode_rust_coverage_toolchain_contract.py|\ + scripts/ci/resolve_base_rust_toolchain.py|\ + tests/test_resolve_base_rust_toolchain.py|\ scripts/ci/materialize_base_javascript_packages.py|\ tests/test_javascript_materializer_docstrings.py|\ docs/doctoring/opencode-rust-coverage-runtime-boundary.md) @@ -379,8 +383,8 @@ jobs: if: steps.affected_suites.outputs.opencode == 'true' run: | set -euo pipefail - python -m pytest -q tests/test_opencode_rust_coverage_toolchain_contract.py - python -m compileall -q tests/test_opencode_rust_coverage_toolchain_contract.py + python -m pytest -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_resolve_base_rust_toolchain.py + python -m compileall -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_resolve_base_rust_toolchain.py - name: Verify JavaScript materializer documentation contract if: steps.affected_suites.outputs.opencode == 'true' diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 26308e1a58..6f8215c7e7 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -817,6 +817,11 @@ jobs: --output-dir "$coverage_build_dir/base-rust-dependencies" \ "${rust_lock_args[@]}" \ --vendor-dir-for-config /opt/base-rust-dependencies/vendor + # Debian's rustc lags the MSRV of pinned repositories (fast-mlsirm pins + # 1.97.1; Debian ships 1.85), so a base-pinned repository gets that exact + # release installed at build time. Unpinned repositories keep Debian's. + base_rust_toolchain="$(python3 -I "$GITHUB_WORKSPACE/scripts/ci/resolve_base_rust_toolchain.py" \ + --repo-root "$COVERAGE_SOURCE_WORKDIR" --base-sha "$PR_BASE_SHA")" cat >"$coverage_build_dir/Dockerfile" <<'DOCKERFILE' FROM docker.io/library/python:3.14-slim@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 ENV DEBIAN_FRONTEND=noninteractive @@ -866,6 +871,27 @@ jobs: && tar -xzf /tmp/cargo-llvm-cov.tar.gz -C /usr/local/bin cargo-llvm-cov \ && chmod 0755 /usr/local/bin/cargo-llvm-cov \ && rm -f /tmp/cargo-llvm-cov.tar.gz + ARG OPENCODE_RUST_TOOLCHAIN= + RUN set -eu; \ + [ -n "$OPENCODE_RUST_TOOLCHAIN" ] || exit 0; \ + printf '%s\n' "$OPENCODE_RUST_TOOLCHAIN" | grep -Eqx '1\.[0-9]{1,3}\.[0-9]{1,3}'; \ + curl --proto '=https' --tlsv1.2 -fsSLo /tmp/rustup-init \ + https://static.rust-lang.org/rustup/archive/1.29.1/x86_64-unknown-linux-gnu/rustup-init; \ + echo 'dda7234360b7f578ca8b0ddcb80145646fa61a67c1720a5abc7051b35c9fcb71 /tmp/rustup-init' | sha256sum -c -; \ + chmod 0755 /tmp/rustup-init; \ + RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/rustup/cargo /tmp/rustup-init -y --no-modify-path \ + --profile minimal --default-toolchain "$OPENCODE_RUST_TOOLCHAIN" --component llvm-tools-preview; \ + rm -f /tmp/rustup-init; \ + toolchain="/usr/local/rustup/toolchains/${OPENCODE_RUST_TOOLCHAIN}-x86_64-unknown-linux-gnu"; \ + ln -s "$toolchain/bin/cargo" /usr/local/bin/cargo; \ + ln -s "$toolchain/bin/rustc" /usr/local/bin/rustc; \ + mkdir -p /usr/local/libexec/opencode-rust; \ + ln -s "$toolchain/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-cov" /usr/local/libexec/opencode-rust/llvm-cov; \ + ln -s "$toolchain/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata" /usr/local/libexec/opencode-rust/llvm-profdata; \ + chmod -R a+rX /usr/local/rustup; \ + test "$(/usr/local/bin/rustc --version | cut -d ' ' -f 2)" = "$OPENCODE_RUST_TOOLCHAIN"; \ + /usr/local/libexec/opencode-rust/llvm-cov --version >/dev/null; \ + /usr/local/libexec/opencode-rust/llvm-profdata --version >/dev/null COPY base-javascript-packages /tmp/base-javascript-packages RUN set -eu; \ mkdir -p /opt/corepack /opt/javascript-package-locks /opt/npm-cache /opt/pnpm-store; \ @@ -956,10 +982,27 @@ jobs: && rm -f /usr/local/libexec/install-base-python-locks.py COPY base-rust-dependencies /opt/base-rust-dependencies DOCKERFILE - if ! docker build --pull --no-cache --network=default \ - --tag "$coverage_tool_image" \ - --file "$coverage_build_dir/Dockerfile" \ - "$coverage_build_dir"; then + build_coverage_tool_image() { + docker build --pull --no-cache --network=default \ + --build-arg "OPENCODE_RUST_TOOLCHAIN=$1" \ + --tag "$coverage_tool_image" \ + --file "$coverage_build_dir/Dockerfile" \ + "$coverage_build_dir" + } + coverage_llvm_cov=/usr/bin/llvm-cov-19 + coverage_llvm_profdata=/usr/bin/llvm-profdata-19 + if [ -n "$base_rust_toolchain" ] && build_coverage_tool_image "$base_rust_toolchain"; then + coverage_llvm_cov=/usr/local/libexec/opencode-rust/llvm-cov + coverage_llvm_profdata=/usr/local/libexec/opencode-rust/llvm-profdata + elif [ -n "$base_rust_toolchain" ]; then + # Rollback: a failed toolchain layer must not cost the repository its + # previous Debian-toolchain image. + echo "::warning::Rust ${base_rust_toolchain} layer failed; rebuilding with the Debian toolchain." + if ! build_coverage_tool_image ""; then + echo "::error::Trusted coverage tool image build failed before PR execution." + exit 1 + fi + elif ! build_coverage_tool_image ""; then echo "::error::Trusted coverage tool image build failed before PR execution." exit 1 fi @@ -997,8 +1040,8 @@ jobs: --env RUNNER_TEMP=/secure-output \ --env GITHUB_OUTPUT=/secure-output/github-output \ --env GITHUB_STEP_SUMMARY=/secure-output/step-summary \ - --env LLVM_COV=/usr/bin/llvm-cov-19 \ - --env LLVM_PROFDATA=/usr/bin/llvm-profdata-19 \ + --env LLVM_COV="$coverage_llvm_cov" \ + --env LLVM_PROFDATA="$coverage_llvm_profdata" \ "$coverage_tool_image" \ /bin/bash /trusted-measure-step.sh || sandbox_status=$? @@ -2077,13 +2120,17 @@ jobs: } ensure_rust_toolchain() { - if [ "${LLVM_COV:-}" != "/usr/bin/llvm-cov-19" ] || \ - [ "${LLVM_PROFDATA:-}" != "/usr/bin/llvm-profdata-19" ] || \ + case "${LLVM_COV:-}:${LLVM_PROFDATA:-}" in + /usr/bin/llvm-cov-19:/usr/bin/llvm-profdata-19) llvm_pair_reviewed=1 ;; + /usr/local/libexec/opencode-rust/llvm-cov:/usr/local/libexec/opencode-rust/llvm-profdata) llvm_pair_reviewed=1 ;; + *) llvm_pair_reviewed=0 ;; + esac + if [ "$llvm_pair_reviewed" != 1 ] || \ ! test -x "$LLVM_COV" || ! test -x "$LLVM_PROFDATA"; then append "### Rust coverage toolchain" append "" append "- Result: FAIL" - append "- Reason: the networkless coverage runtime did not preserve the reviewed LLVM 19 tool paths." + append "- Reason: the networkless coverage runtime did not preserve the reviewed LLVM tool paths (Debian LLVM 19 or the base-pinned Rust toolchain)." append "- Fix: rebuild the trusted coverage image and preserve the exact LLVM bindings at the Docker boundary." append "" failures=$((failures + 1)) diff --git a/CHANGELOG.d/20260930-coverage-base-pinned-rust.md b/CHANGELOG.d/20260930-coverage-base-pinned-rust.md new file mode 100644 index 0000000000..4560a7e5df --- /dev/null +++ b/CHANGELOG.d/20260930-coverage-base-pinned-rust.md @@ -0,0 +1,10 @@ +### Coverage sandbox installs the base-pinned Rust release + +- The coverage image shipped Debian's rustc 1.85, but fast-mlsirm pins 1.97.1 and its + dependencies need at least 1.90. `maturin build --offline` failed, `_core` never imported, and + every fast-mlsirm coverage run fell to about 68% against its 100% gate, so no fast-mlsirm pull + request could reach an APPROVED review. When the base commit pins an exact `1.x.y` release in + `rust-toolchain(.toml)`, the image now installs it from a SHA-256-verified `rustup-init` + (minimal profile plus `llvm-tools-preview`) and binds Rust coverage to that release's LLVM tools. + Repositories without a pin keep the Debian toolchain, and a failed toolchain layer rebuilds the + previous image. See `docs/doctoring/opencode-rust-coverage-runtime-boundary.md`. diff --git a/docs/doctoring/opencode-rust-coverage-runtime-boundary.md b/docs/doctoring/opencode-rust-coverage-runtime-boundary.md index 8adc68d0ea..04213ba398 100644 --- a/docs/doctoring/opencode-rust-coverage-runtime-boundary.md +++ b/docs/doctoring/opencode-rust-coverage-runtime-boundary.md @@ -18,9 +18,35 @@ declarations and the isolated container's `docker run --env` arguments. If that workflow changes, its `REVIEW_DISPATCH_BLOB_SHA` pin must change with it; this does not rewrite the review-agent key system. +### Base-pinned Rust releases (2026-09-30) + +Debian's `rustc` (1.85) is older than the MSRV of repositories that pin a newer +release; fast-mlsirm pins 1.97.1 and its dependency graph needs at least 1.90, +so `maturin build --offline` could never build `_core` and coverage evidence +failed for every fast-mlsirm pull request. When the live-validated **base** +commit carries `rust-toolchain.toml` (or `rust-toolchain`), +`scripts/ci/resolve_base_rust_toolchain.py` selects its exact `1.x.y` channel; +`stable`, `nightly-*`, custom `path` toolchains and unparsable files select the +central pin instead. The pull-request head's pin is never read. The image build +(network allowed, before any PR content is mounted) then downloads +`rustup-init` 1.29.1 from `static.rust-lang.org`, verifies its SHA-256, and +installs only that release with `--profile minimal --component +llvm-tools-preview`; rustup checks each component against the hashes in the +channel manifest. `cargo` and `rustc` are symlinked straight to the toolchain +binaries, so no rustup proxy runs inside the `--network=none` sandbox, and the +second reviewed LLVM pair becomes: + +- `LLVM_COV=/usr/local/libexec/opencode-rust/llvm-cov` +- `LLVM_PROFDATA=/usr/local/libexec/opencode-rust/llvm-profdata` + +Those tools ship with the same rustc release, so they match its profile format. +Repositories without a base pin keep the Debian image unchanged, and if the +pinned layer fails to build, the job rebuilds the Debian image rather than +losing coverage for that repository. + The runtime MUST NOT fall back to unversioned `llvm-cov` or `llvm-profdata`, a -host-runner tool, a pull-request-selected path, or a dynamically downloaded LLVM -binary. Missing, changed, or non-executable reviewed paths are coverage-evidence +host-runner tool, a pull-request-selected path, or an LLVM binary downloaded at +PR runtime. Missing, changed, or non-executable reviewed paths are coverage-evidence failures rather than reasons to measure a different toolchain. NIST SP 800-218 PW.4.1 covers acquiring and maintaining third-party software diff --git a/scripts/ci/resolve_base_rust_toolchain.py b/scripts/ci/resolve_base_rust_toolchain.py new file mode 100755 index 0000000000..6dcd1642c2 --- /dev/null +++ b/scripts/ci/resolve_base_rust_toolchain.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Print the exact Rust release the trusted coverage image must install. + +The pin is read only from the live-validated BASE commit, never from the pull +request head. An empty result keeps the image's Debian toolchain unchanged; a +base pin that is not an exact stable release (``stable``, ``nightly-*``, a +custom ``path``, or unparsable TOML) falls back to ``CENTRAL_RUST_TOOLCHAIN``. +""" + +from __future__ import annotations + +import argparse +import pathlib +import re +import subprocess +import sys +import tomllib + +SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") +EXACT_RELEASE_RE = re.compile(r"1\.[0-9]{1,3}\.[0-9]{1,3}") +CENTRAL_RUST_TOOLCHAIN = "1.97.1" +PIN_FILES = ("rust-toolchain.toml", "rust-toolchain") + + +def _base_blob(repo_root: pathlib.Path, base_sha: str, path: str) -> str | None: + completed = subprocess.run( + ["git", "-C", str(repo_root), "show", f"{base_sha}:{path}"], + check=False, + capture_output=True, + ) + if completed.returncode != 0: + return None + return completed.stdout.decode("utf-8", errors="replace") + + +def _channel(content: str) -> str | None: + try: + toolchain = tomllib.loads(content).get("toolchain") + except tomllib.TOMLDecodeError: + # The legacy `rust-toolchain` file may be a bare channel line. + lines = content.split() + return lines[0] if len(lines) == 1 else None + if not isinstance(toolchain, dict) or "path" in toolchain: + return None + channel = toolchain.get("channel") + return channel if isinstance(channel, str) else None + + +def resolve(repo_root: pathlib.Path, base_sha: str) -> str: + if not SHA_RE.fullmatch(base_sha): + raise ValueError("base SHA must be a full 40-character commit id") + for path in PIN_FILES: + content = _base_blob(repo_root, base_sha, path) + if content is None: + continue + channel = _channel(content) + if channel and EXACT_RELEASE_RE.fullmatch(channel): + return channel + print( + f"::warning::{path} at the base SHA does not pin an exact stable Rust release; " + f"using the central {CENTRAL_RUST_TOOLCHAIN} toolchain.", + file=sys.stderr, + ) + return CENTRAL_RUST_TOOLCHAIN + return "" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repo-root", required=True, type=pathlib.Path) + parser.add_argument("--base-sha", required=True) + args = parser.parse_args(argv) + try: + print(resolve(args.repo_root, args.base_sha)) + except ValueError as exc: + print(f"::error::{exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_opencode_rust_coverage_toolchain_contract.py b/tests/test_opencode_rust_coverage_toolchain_contract.py index cc0c49af6f..757fa80500 100644 --- a/tests/test_opencode_rust_coverage_toolchain_contract.py +++ b/tests/test_opencode_rust_coverage_toolchain_contract.py @@ -27,6 +27,8 @@ ) _LLVM_COV_PATH = "/usr/bin/llvm-cov-19" _LLVM_PROFDATA_PATH = "/usr/bin/llvm-profdata-19" +_PINNED_LLVM_COV_PATH = "/usr/local/libexec/opencode-rust/llvm-cov" +_PINNED_LLVM_PROFDATA_PATH = "/usr/local/libexec/opencode-rust/llvm-profdata" _BLOB_SHA_PATTERN = re.compile( r'^REVIEW_DISPATCH_BLOB_SHA = "([0-9a-f]{40})"$', re.MULTILINE, @@ -75,8 +77,12 @@ def test_isolated_runtime_receives_reviewed_llvm_constants() -> None: dispatch = _dispatch_text() helper = _helper_text() - assert f" --env LLVM_COV={_LLVM_COV_PATH} \\\n" in dispatch - assert f" --env LLVM_PROFDATA={_LLVM_PROFDATA_PATH} \\\n" in dispatch + assert ' --env LLVM_COV="$coverage_llvm_cov" \\\n' in dispatch + assert ' --env LLVM_PROFDATA="$coverage_llvm_profdata" \\\n' in dispatch + assert f" coverage_llvm_cov={_LLVM_COV_PATH}\n" in dispatch + assert f" coverage_llvm_profdata={_LLVM_PROFDATA_PATH}\n" in dispatch + assert f" coverage_llvm_cov={_PINNED_LLVM_COV_PATH}\n" in dispatch + assert f" coverage_llvm_profdata={_PINNED_LLVM_PROFDATA_PATH}\n" in dispatch assert _LLVM_COV_PATH in helper assert _LLVM_PROFDATA_PATH in helper assert "unversioned" not in helper @@ -89,11 +95,15 @@ def test_isolated_runtime_revalidates_llvm_tools_before_coverage() -> None: dispatch = _dispatch_text() helper = _helper_text() - assert f'[ "${{LLVM_COV:-}}" != "{_LLVM_COV_PATH}" ]' in dispatch - assert f'[ "${{LLVM_PROFDATA:-}}" != "{_LLVM_PROFDATA_PATH}" ]' in dispatch + assert f" {_LLVM_COV_PATH}:{_LLVM_PROFDATA_PATH}) llvm_pair_reviewed=1 ;;\n" in dispatch + assert ( + f" {_PINNED_LLVM_COV_PATH}:{_PINNED_LLVM_PROFDATA_PATH}) llvm_pair_reviewed=1 ;;\n" + in dispatch + ) + assert " *) llvm_pair_reviewed=0 ;;\n" in dispatch assert 'test -x "$LLVM_COV"' in dispatch assert 'test -x "$LLVM_PROFDATA"' in dispatch - assert "networkless coverage runtime did not preserve the reviewed LLVM 19" in dispatch + assert "networkless coverage runtime did not preserve the reviewed LLVM tool paths" in dispatch assert f'LLVM_COV_PATH="{_LLVM_COV_PATH}"' in helper assert f'LLVM_PROFDATA_PATH="{_LLVM_PROFDATA_PATH}"' in helper assert '"${LLVM_COV:-}" != "$LLVM_COV_PATH"' in helper @@ -101,6 +111,40 @@ def test_isolated_runtime_revalidates_llvm_tools_before_coverage() -> None: assert "exit 1" in helper +def test_base_pinned_rust_layer_is_verified_offline_and_optional() -> None: + """A base pin installs one exact release from a hash-verified rustup-init.""" + + dispatch = _dispatch_text() + assert "--repo-root \"$COVERAGE_SOURCE_WORKDIR\" --base-sha \"$PR_BASE_SHA\")\"" in dispatch + assert " ARG OPENCODE_RUST_TOOLCHAIN=\n" in dispatch + assert ' [ -n "$OPENCODE_RUST_TOOLCHAIN" ] || exit 0; \\\n' in dispatch + assert "grep -Eqx '1\\.[0-9]{1,3}\\.[0-9]{1,3}'" in dispatch + assert ( + "https://static.rust-lang.org/rustup/archive/1.29.1/x86_64-unknown-linux-gnu/rustup-init" + in dispatch + ) + assert ( + "echo 'dda7234360b7f578ca8b0ddcb80145646fa61a67c1720a5abc7051b35c9fcb71 /tmp/rustup-init'" + " | sha256sum -c -" in dispatch + ) + assert "--profile minimal --default-toolchain \"$OPENCODE_RUST_TOOLCHAIN\" --component llvm-tools-preview" in dispatch + assert not re.search(r"\| *(ba)?sh\b", dispatch.split("ARG OPENCODE_RUST_TOOLCHAIN=", 1)[1].split("COPY", 1)[0]) + # The sandbox calls the toolchain binaries directly, so no rustup proxy can + # try to resolve (and download) a toolchain inside --network=none. + assert 'ln -s "$toolchain/bin/cargo" /usr/local/bin/cargo;' in dispatch + assert 'ln -s "$toolchain/bin/rustc" /usr/local/bin/rustc;' in dispatch + + +def test_unpinned_repositories_build_the_previous_image() -> None: + """No base pin, or a failed pinned layer, rebuilds the Debian-toolchain image.""" + + dispatch = _dispatch_text() + assert 'if [ -n "$base_rust_toolchain" ] && build_coverage_tool_image "$base_rust_toolchain"; then' in dispatch + assert 'if ! build_coverage_tool_image ""; then' in dispatch + assert 'elif ! build_coverage_tool_image ""; then' in dispatch + assert "rebuilding with the Debian toolchain." in dispatch + + def test_quality_workflow_watches_the_trusted_dispatch_workflow() -> None: """Guard drift in the hashed review-dispatch blob must retrigger this contract.""" diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 6e1918ed42..f912302945 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 = "26308e1a58f37f8aa15b3dd6d0453002ea1f50e8" +REVIEW_DISPATCH_BLOB_SHA = "6f8215c7e7e22ae8389713fdf74dbc4855448807" def _workflow_text(path: Path) -> str: diff --git a/tests/test_resolve_base_rust_toolchain.py b/tests/test_resolve_base_rust_toolchain.py new file mode 100644 index 0000000000..db47136c80 --- /dev/null +++ b/tests/test_resolve_base_rust_toolchain.py @@ -0,0 +1,83 @@ +"""The coverage image installs only an exact Rust release pinned on the trusted base.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from scripts.ci import resolve_base_rust_toolchain as resolver + + +def _git(repo: Path, *args: str) -> str: + return subprocess.run( + ["git", "-C", str(repo), *args], check=True, capture_output=True, text=True + ).stdout.strip() + + +def _commit(repo: Path, files: dict[str, str]) -> str: + if not (repo / ".git").exists(): + repo.mkdir(parents=True, exist_ok=True) + _git(repo, "init", "-q") + _git(repo, "config", "user.name", "Test") + _git(repo, "config", "user.email", "test@example.invalid") + for name, content in files.items(): + (repo / name).write_text(content, encoding="utf-8") + _git(repo, "add", "-A") + _git(repo, "commit", "-q", "--allow-empty", "-m", "fixture") + return _git(repo, "rev-parse", "HEAD") + + +def test_unpinned_base_keeps_the_distribution_toolchain(tmp_path: Path) -> None: + base = _commit(tmp_path, {"README.md": "no pin\n"}) + assert resolver.resolve(tmp_path, base) == "" + + +@pytest.mark.parametrize( + ("name", "content"), + [ + ("rust-toolchain.toml", '[toolchain]\nchannel = "1.97.1"\nprofile = "minimal"\n'), + ("rust-toolchain", "1.97.1\n"), + ("rust-toolchain", '[toolchain]\nchannel = "1.97.1"\n'), + ], +) +def test_exact_base_release_is_selected(tmp_path: Path, name: str, content: str) -> None: + base = _commit(tmp_path, {name: content}) + assert resolver.resolve(tmp_path, base) == "1.97.1" + + +@pytest.mark.parametrize( + "content", + [ + '[toolchain]\nchannel = "stable"\n', + '[toolchain]\nchannel = "nightly-2026-09-01"\n', + '[toolchain]\nchannel = "1.97"\n', + '[toolchain]\nchannel = "1.97.1; curl evil"\n', + '[toolchain]\nchannel = "../../evil"\n', + '[toolchain]\npath = "/opt/custom"\nchannel = "1.97.1"\n', + "[toolchain\n", + "", + ], +) +def test_unsupported_base_pins_fall_back_to_the_central_release( + tmp_path: Path, content: str +) -> None: + base = _commit(tmp_path, {"rust-toolchain.toml": content}) + assert resolver.resolve(tmp_path, base) == resolver.CENTRAL_RUST_TOOLCHAIN + + +def test_pull_request_head_pin_is_ignored(tmp_path: Path) -> None: + base = _commit(tmp_path, {"rust-toolchain.toml": '[toolchain]\nchannel = "1.97.1"\n'}) + _commit(tmp_path, {"rust-toolchain.toml": '[toolchain]\nchannel = "1.80.0"\n'}) + assert resolver.resolve(tmp_path, base) == "1.97.1" + + +def test_invalid_base_sha_is_rejected(tmp_path: Path) -> None: + _commit(tmp_path, {"README.md": "x\n"}) + with pytest.raises(ValueError): + resolver.resolve(tmp_path, "HEAD") + + +def test_central_release_is_itself_exact() -> None: + assert resolver.EXACT_RELEASE_RE.fullmatch(resolver.CENTRAL_RUST_TOOLCHAIN) From 048921882f03d37ca1141520d1a46a8f2f7521a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 30 Sep 2026 05:01:01 +0900 Subject: [PATCH 2/3] fix(coverage): resolve the base Rust pin next to the image build The head-lock intake contract executes the workflow block that ends at the Dockerfile heredoc with a python3 stub; resolving the toolchain inside that block overwrote its argument receipt. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PtgtJk1WjqieqvLDb1w8uW --- .github/workflows/opencode-review-dispatch.yml | 10 +++++----- tests/test_pr_review_autofix_nvidia_nim_contract.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 6f8215c7e7..1cbb991de0 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -817,11 +817,6 @@ jobs: --output-dir "$coverage_build_dir/base-rust-dependencies" \ "${rust_lock_args[@]}" \ --vendor-dir-for-config /opt/base-rust-dependencies/vendor - # Debian's rustc lags the MSRV of pinned repositories (fast-mlsirm pins - # 1.97.1; Debian ships 1.85), so a base-pinned repository gets that exact - # release installed at build time. Unpinned repositories keep Debian's. - base_rust_toolchain="$(python3 -I "$GITHUB_WORKSPACE/scripts/ci/resolve_base_rust_toolchain.py" \ - --repo-root "$COVERAGE_SOURCE_WORKDIR" --base-sha "$PR_BASE_SHA")" cat >"$coverage_build_dir/Dockerfile" <<'DOCKERFILE' FROM docker.io/library/python:3.14-slim@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 ENV DEBIAN_FRONTEND=noninteractive @@ -982,6 +977,11 @@ jobs: && rm -f /usr/local/libexec/install-base-python-locks.py COPY base-rust-dependencies /opt/base-rust-dependencies DOCKERFILE + # Debian's rustc lags the MSRV of pinned repositories (fast-mlsirm pins + # 1.97.1; Debian ships 1.85), so a base-pinned repository gets that exact + # release installed at build time. Unpinned repositories keep Debian's. + base_rust_toolchain="$(python3 -I "$GITHUB_WORKSPACE/scripts/ci/resolve_base_rust_toolchain.py" \ + --repo-root "$COVERAGE_SOURCE_WORKDIR" --base-sha "$PR_BASE_SHA")" build_coverage_tool_image() { docker build --pull --no-cache --network=default \ --build-arg "OPENCODE_RUST_TOOLCHAIN=$1" \ diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index f912302945..5afab8f302 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 = "6f8215c7e7e22ae8389713fdf74dbc4855448807" +REVIEW_DISPATCH_BLOB_SHA = "1cbb991de09de20186ab0644ee171dba02270180" def _workflow_text(path: Path) -> str: From 2c90c581d0f74322708e832f51aad5f61c0947d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 30 Sep 2026 05:26:35 +0900 Subject: [PATCH 3/3] fix(coverage): place the built PyO3 extension where pytest imports it fast-mlsirm's pytest pythonpath puts python/ first, so the source-tree package shadowed the wheel installed into user site-packages and _core stayed unimportable even after a successful offline build. Copy only the wheel's compiled extension members into the tool.maturin.python-source package, as maturin develop does, rejecting paths that leave the project. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PtgtJk1WjqieqvLDb1w8uW --- .../agent-review-runtime-quality-ci.yml | 8 +- .../workflows/opencode-review-dispatch.yml | 3 +- .../20260930-coverage-base-pinned-rust.md | 4 + scripts/ci/place_maturin_extension.py | 65 +++++++++++++ tests/test_place_maturin_extension.py | 91 +++++++++++++++++++ ...t_pr_review_autofix_nvidia_nim_contract.py | 2 +- 6 files changed, 169 insertions(+), 4 deletions(-) create mode 100755 scripts/ci/place_maturin_extension.py create mode 100644 tests/test_place_maturin_extension.py diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index f3b9200414..0465937b92 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -23,6 +23,8 @@ on: - "tests/test_opencode_rust_coverage_toolchain_contract.py" - "scripts/ci/resolve_base_rust_toolchain.py" - "tests/test_resolve_base_rust_toolchain.py" + - "scripts/ci/place_maturin_extension.py" + - "tests/test_place_maturin_extension.py" - "scripts/ci/materialize_base_javascript_packages.py" - "tests/test_javascript_materializer_docstrings.py" - "tests/test_pr_review_autofix_nvidia_nim_contract.py" @@ -208,6 +210,8 @@ jobs: tests/test_opencode_rust_coverage_toolchain_contract.py|\ scripts/ci/resolve_base_rust_toolchain.py|\ tests/test_resolve_base_rust_toolchain.py|\ + scripts/ci/place_maturin_extension.py|\ + tests/test_place_maturin_extension.py|\ scripts/ci/materialize_base_javascript_packages.py|\ tests/test_javascript_materializer_docstrings.py|\ docs/doctoring/opencode-rust-coverage-runtime-boundary.md) @@ -383,8 +387,8 @@ jobs: if: steps.affected_suites.outputs.opencode == 'true' run: | set -euo pipefail - python -m pytest -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_resolve_base_rust_toolchain.py - python -m compileall -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_resolve_base_rust_toolchain.py + python -m pytest -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py + python -m compileall -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py - name: Verify JavaScript materializer documentation contract if: steps.affected_suites.outputs.opencode == 'true' diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 1cbb991de0..779750f1f6 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -1398,7 +1398,8 @@ jobs: dist_dir="$(mktemp -d)" python3 -m maturin build --offline --release -o "$dist_dir" python3 -m pip install --user --no-index --no-deps --force-reinstall "$dist_dir"/*.whl - rm -rf "$dist_dir"' bash "$project_dir" + python3 "$2" "$dist_dir"/*.whl . + rm -rf "$dist_dir"' bash "$project_dir" "${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py" } run_python_test_coverage() { diff --git a/CHANGELOG.d/20260930-coverage-base-pinned-rust.md b/CHANGELOG.d/20260930-coverage-base-pinned-rust.md index 4560a7e5df..55eaa3fb02 100644 --- a/CHANGELOG.d/20260930-coverage-base-pinned-rust.md +++ b/CHANGELOG.d/20260930-coverage-base-pinned-rust.md @@ -8,3 +8,7 @@ (minimal profile plus `llvm-tools-preview`) and binds Rust coverage to that release's LLVM tools. Repositories without a pin keep the Debian toolchain, and a failed toolchain layer rebuilds the previous image. See `docs/doctoring/opencode-rust-coverage-runtime-boundary.md`. +- The offline maturin build now also copies the wheel's compiled extension into the project's + `tool.maturin.python-source` package (`scripts/ci/place_maturin_extension.py`), as + `maturin develop` would. fast-mlsirm's pytest `pythonpath = ["python"]` imports the source tree + first, so a `_core` present only in site-packages stayed shadowed even after a successful build. diff --git a/scripts/ci/place_maturin_extension.py b/scripts/ci/place_maturin_extension.py new file mode 100755 index 0000000000..45ff27aba5 --- /dev/null +++ b/scripts/ci/place_maturin_extension.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Copy a built wheel's extension modules into the project's Python source tree. + +Projects such as fast-mlsirm set pytest ``pythonpath = ["python"]``, so the test +suite imports ``python/`` rather than the installed wheel, and an +extension that exists only in site-packages is shadowed (``cannot import name +'_core'``). This mirrors what ``maturin develop`` does: only compiled extension +members are copied, only into package directories the source tree already has. +""" + +from __future__ import annotations + +import pathlib +import sys +import tomllib +import zipfile + +EXTENSION_SUFFIXES = (".so", ".pyd") + + +def _inside(path: pathlib.Path, root: pathlib.Path) -> bool: + return path == root or root in path.parents + + +def place(wheel: pathlib.Path, project_dir: pathlib.Path) -> list[pathlib.Path]: + project = project_dir.resolve() + pyproject = tomllib.loads((project / "pyproject.toml").read_text(encoding="utf-8")) + python_source = pyproject.get("tool", {}).get("maturin", {}).get("python-source", ".") + if not isinstance(python_source, str): + raise ValueError("tool.maturin.python-source must be a string") + source_root = (project / python_source).resolve() + if not _inside(source_root, project): + raise ValueError("tool.maturin.python-source escapes the project directory") + + placed = [] + with zipfile.ZipFile(wheel) as archive: + for member in archive.infolist(): + if member.is_dir() or not member.filename.endswith(EXTENSION_SUFFIXES): + continue + target = (source_root / member.filename).resolve() + if not _inside(target, source_root): + raise ValueError(f"wheel member escapes the source tree: {member.filename}") + if not target.parent.is_dir(): + continue + target.write_bytes(archive.read(member)) + placed.append(target) + return placed + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print("usage: place_maturin_extension.py WHEEL PROJECT_DIR", file=sys.stderr) + return 2 + try: + placed = place(pathlib.Path(argv[0]), pathlib.Path(argv[1])) + except (OSError, ValueError, zipfile.BadZipFile, tomllib.TOMLDecodeError) as exc: + print(f"Could not place the built extension in the source tree: {exc}", file=sys.stderr) + return 1 + for path in placed: + print(f"Placed built extension at {path}") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tests/test_place_maturin_extension.py b/tests/test_place_maturin_extension.py new file mode 100644 index 0000000000..a1d6f7757a --- /dev/null +++ b/tests/test_place_maturin_extension.py @@ -0,0 +1,91 @@ +"""A built PyO3 extension must be importable from the source tree pytest actually imports.""" + +from __future__ import annotations + +import zipfile +from pathlib import Path + +import pytest + +from scripts.ci import place_maturin_extension as placer + +_SO = "_core.cpython-314-x86_64-linux-gnu.so" + + +def _project(tmp_path: Path, python_source: str | None) -> Path: + project = tmp_path / "project" + maturin = f'[tool.maturin]\npython-source = "{python_source}"\n' if python_source else "" + (project).mkdir() + (project / "pyproject.toml").write_text( + '[build-system]\nbuild-backend = "maturin"\n' + maturin, encoding="utf-8" + ) + package = project / (python_source or ".") / "pkg" + package.mkdir(parents=True) + (package / "__init__.py").write_text("from . import _core\n", encoding="utf-8") + return project + + +def _wheel(tmp_path: Path, members: dict[str, bytes]) -> Path: + wheel = tmp_path / "pkg-0.1.0-cp314-cp314-linux_x86_64.whl" + with zipfile.ZipFile(wheel, "w") as archive: + for name, data in members.items(): + archive.writestr(name, data) + return wheel + + +@pytest.mark.parametrize("python_source", ["python", None]) +def test_extension_lands_in_the_imported_source_package( + tmp_path: Path, python_source: str | None +) -> None: + project = _project(tmp_path, python_source) + wheel = _wheel( + tmp_path, + { + f"pkg/{_SO}": b"ELF", + "pkg/__init__.py": b"# wheel copy must not overwrite the PR source\n", + "pkg-0.1.0.dist-info/RECORD": b"", + }, + ) + placed = placer.place(wheel, project) + target = project / (python_source or ".") / "pkg" / _SO + assert placed == [target.resolve()] + assert target.read_bytes() == b"ELF" + assert (project / (python_source or ".") / "pkg/__init__.py").read_text( + encoding="utf-8" + ) == "from . import _core\n" + + +def test_python_source_outside_the_project_is_rejected(tmp_path: Path) -> None: + project = _project(tmp_path, "python") + (project / "pyproject.toml").write_text( + '[tool.maturin]\npython-source = "../outside"\n', encoding="utf-8" + ) + wheel = _wheel(tmp_path, {f"pkg/{_SO}": b"ELF"}) + with pytest.raises(ValueError): + placer.place(wheel, project) + + +def test_traversing_wheel_member_is_rejected(tmp_path: Path) -> None: + project = _project(tmp_path, "python") + wheel = _wheel(tmp_path, {f"../../{_SO}": b"ELF"}) + with pytest.raises(ValueError): + placer.place(wheel, project) + assert not (tmp_path / _SO).exists() + + +def test_extension_for_a_package_absent_from_the_source_tree_is_skipped( + tmp_path: Path, +) -> None: + project = _project(tmp_path, "python") + wheel = _wheel(tmp_path, {f"other/{_SO}": b"ELF"}) + assert placer.place(wheel, project) == [] + assert not (project / "python/other").exists() + + +def test_offline_maturin_build_places_the_extension_for_pytest() -> None: + workflow = ( + Path(__file__).resolve().parents[1] / ".github/workflows/opencode-review-dispatch.yml" + ).read_text(encoding="utf-8") + build = workflow.split("build_maturin_extension_if_needed() {", 1)[1].split("\n }", 1)[0] + assert 'python3 "$2" "$dist_dir"/*.whl .' in build + assert '"${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py"' in build diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 5afab8f302..a0284e1f62 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 = "1cbb991de09de20186ab0644ee171dba02270180" +REVIEW_DISPATCH_BLOB_SHA = "779750f1f605282370cd842538af8707b3e83b2e" def _workflow_text(path: Path) -> str: