From 539a5176c56b57cf326e067441f566a6d63475fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:05:17 +0900 Subject: [PATCH 01/16] ci(fast-mlsirm): centralize Linux workflows on isolated self-hosted runners --- .github/workflows/fast-mlsirm-ci.yml | 285 ++++++++++++++++++ .github/workflows/fast-mlsirm-codeql.yml | 63 ++++ .../fast-mlsirm-hourly-pr-governance.yml | 163 ++++++++++ .../workflows/fast-mlsirm-pypi-gap-guard.yml | 166 ++++++++++ .../fast-mlsirm-statistical-studies.yml | 170 +++++++++++ 5 files changed, 847 insertions(+) create mode 100644 .github/workflows/fast-mlsirm-ci.yml create mode 100644 .github/workflows/fast-mlsirm-codeql.yml create mode 100644 .github/workflows/fast-mlsirm-hourly-pr-governance.yml create mode 100644 .github/workflows/fast-mlsirm-pypi-gap-guard.yml create mode 100644 .github/workflows/fast-mlsirm-statistical-studies.yml diff --git a/.github/workflows/fast-mlsirm-ci.yml b/.github/workflows/fast-mlsirm-ci.yml new file mode 100644 index 0000000000..53d6324a85 --- /dev/null +++ b/.github/workflows/fast-mlsirm-ci.yml @@ -0,0 +1,285 @@ +name: CI + +on: + workflow_call: + +permissions: + contents: read + +concurrency: + # Keep one active CI run per PR while preserving independent push runs. A + # synchronized head cancels its queued/running predecessor instead of + # multiplying the hosted-runner backlog with stale evidence. + group: central-fast-mlsirm-ci-${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + # Matrix legs report as "python (3.12)" / "python (3.14)". Branch protection + # still requires the exact check context name `python`, so a thin aggregate + # job below re-exports matrix success under that name. + python-matrix: + if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} + name: python (${{ matrix.python-version }}) + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.14"] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: ${{ matrix.python-version }} + # Rust toolchain is required so the maturin build compiles the primary + # numeric core (fast_mlsirm._core) during the editable install; the + # Rust<->NumPy parity gate then exercises the Rust path. + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.97.1 + - run: python -m pip install --require-hashes -r requirements/ci.txt + - run: python -m pip install --no-deps --no-build-isolation -e . + - name: Verify Rust core is the resolved default backend + run: python -c "import fast_mlsirm._core as c; from fast_mlsirm.backend import resolve_backend; from fast_mlsirm import FitConfig; assert hasattr(c, 'neg_loglik_and_grad'); assert resolve_backend(FitConfig().backend) == 'rust'; print('rust primary OK')" + - run: pytest + + python: + name: python + needs: python-matrix + if: ${{ always() && (github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed')) }} + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + steps: + - name: Require every CPython matrix leg + run: | + echo "python-matrix result=${{ needs.python-matrix.result }}" + test "${{ needs.python-matrix.result }}" = "success" + + + rust: + if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: "3.12" + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.97.1 + # Ordinary Rust tests include the deterministic equation and bounded + # recovery-contract sentinels. Exhaustive ignored studies run in the + # scheduled Statistical Studies workflow rather than on every PR. + # --locked fails the job when a Cargo.lock would have to change to build. + # crates/fast-mlsirm-py is excluded from the root workspace and carries its + # own lock, so without this a dependency added there resolves fresh at build + # time and the committed lock the security gate scans silently falls behind + # the graph that actually ships. + - run: cargo test --locked --workspace + - run: cargo test --locked --manifest-path crates/fast-mlsirm-py/Cargo.toml + + gpu-smoke: + if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} + # Exact GPU and study-precision nodes below own every corresponding + # non-execution allowlist entry. The full paper-design recovery study + # remains in the scheduled/manual workflow. + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 240 + env: + WGPU_BACKEND: vulkan + XDG_RUNTIME_DIR: /tmp/fast-mlsirm-xdg-runtime + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: "3.12" + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.97.1 + - name: Install software Vulkan adapter + run: | + # The hosted runner's mirror+file registry can keep selecting a + # black-holed Azure mirror even after apt falls back for InRelease. + # Use a job-local source file pinned to canonical Ubuntu archives so + # package payloads do not re-enter that broken mirror selection path. + . /etc/os-release + test -n "${VERSION_CODENAME:-}" + cat > /tmp/fast-mlsirm-ubuntu.sources <> "$GITHUB_ENV" + - name: Prove Vulkan compute adapter availability + run: vulkaninfo --summary + - run: python -m pip install --require-hashes -r requirements/ci.txt + - run: python -m pip install --no-deps --no-build-isolation -e . + - name: Run exact GPU and study-precision evidence without a skip + env: + STAGE5_HIGH_Q: "1" + STAGE5_HIGH_Q_BOOTSTRAP_REPS: "2" + run: | + pytest -q --junitxml=gpu-junit.xml \ + tests/test_marginal_parity.py::test_marginal_gpu_agrees_with_cpu_loosely \ + tests/test_bifactor_gpu_high_q.py::test_bifactor_gpu_parity_q121 \ + tests/test_bifactor_gpu_high_q.py::test_bifactor_gpu_parity_q241 \ + tests/test_bifactor_gpu_high_q.py::test_bifactor_gpu_parity_q481 \ + tests/test_bifactor_gpu_high_q.py::test_bifactor_gpu_parity_q241_wide_items_metal_workgroups \ + tests/test_bifactor_gpu_high_q.py::test_bifactor_cpu_q121_vs_q241_agree \ + tests/test_bifactor_bootstrap_benchmark.py::test_joint_bootstrap_cpu_vs_gpu_wall_time_q121 + - name: Reject skipped GPU evidence + run: | + python - <<'PY' + from pathlib import Path + from xml.etree import ElementTree + + root = ElementTree.parse(Path("gpu-junit.xml")).getroot() + suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite")) + tests = sum(int(suite.attrib.get("tests", "0")) for suite in suites) + skipped = sum(int(suite.attrib.get("skipped", "0")) for suite in suites) + if tests < 1: + raise SystemExit("GPU evidence did not execute a test") + if skipped != 0: + raise SystemExit(f"GPU evidence contained {skipped} skipped test(s)") + PY + + fuzz: + if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} + # Short, bounded coverage-guided fuzzing so PR cost stays low. The + # property-based Hypothesis tests already run in the `python` job (via the + # dev extra); this job adds the Atheris coverage-guided harnesses with a + # ~60s-per-target budget. Longer campaigns can be run nightly if desired. + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: "3.12" + # pip is version-pinned so the toolchain used to resolve the fuzz extras + # is reproducible (OpenSSF Scorecard pinned-dependencies hardening). + - run: python -m pip install --upgrade "pip==26.1.2" + # The property checks below use pytest from the dev extra; fuzz alone + # provides Atheris and Hypothesis, but not the pytest command. + # Editable/local installs cannot use --require-hashes. + - run: python -m pip install -e '.[dev,fuzz]' + - name: Run Hypothesis input properties without a skip + run: pytest tests/test_fuzz_properties.py -q --junitxml=fuzz-properties-junit.xml + - name: Reject skipped Hypothesis evidence + run: | + python - <<'PY' + from pathlib import Path + from xml.etree import ElementTree + + root = ElementTree.parse(Path("fuzz-properties-junit.xml")).getroot() + suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite")) + tests = sum(int(suite.attrib.get("tests", "0")) for suite in suites) + skipped = sum(int(suite.attrib.get("skipped", "0")) for suite in suites) + if tests < 1: + raise SystemExit("Hypothesis evidence did not execute a test") + if skipped != 0: + raise SystemExit( + f"Hypothesis evidence contained {skipped} skipped test(s)" + ) + PY + - name: Fuzz load_factor_csv (CSV parser) + run: | + python fuzz/atheris/fuzz_load_factor_csv.py \ + -max_total_time=60 -timeout=25 fuzz/corpus/load_factor_csv + - name: Fuzz render_diagnostics_report (JSON -> HTML) + run: | + python fuzz/atheris/fuzz_render_report.py \ + -max_total_time=60 -timeout=25 fuzz/corpus/render_report + - name: Fuzz config validators + run: | + python fuzz/atheris/fuzz_config.py \ + -max_total_time=60 -timeout=25 fuzz/corpus/config + + package: + if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: "3.12" + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.97.1 + - run: python -m pip install --require-hashes -r requirements/package.txt + - run: python -m build --no-isolation + - run: python -m pip install --no-index --no-deps --force-reinstall dist/*.whl + - run: python -c "import fast_mlsirm, fast_mlsirm._core as core; print(fast_mlsirm.__version__, hasattr(core, 'neg_loglik_and_grad'))" + - name: Run release acceptance smoke + run: | + python scripts/release_acceptance.py --out release-acceptance --require-rust --persons 12 --dims 1 --items-per-dim 2 --latent-dim 1 --latent-dims 1,2 --folds 2 --max-iter 1 --n-restarts 1 + python - <<'PY' + import json + + with open("release-acceptance/acceptance_summary.json", encoding="utf-8") as fh: + summary = json.load(fh) + assert summary["status"] == "ok" + PY + - name: Run enterprise sales readiness gate + run: | + python scripts/sales_readiness.py \ + --acceptance release-acceptance/acceptance_summary.json \ + --dist dist \ + --require-rust \ + --require-20b-product \ + --check-import \ + --out release-acceptance/sales_readiness_manifest.json + python - <<'PY' + import json + + with open("release-acceptance/sales_readiness_manifest.json", encoding="utf-8") as fh: + manifest = json.load(fh) + assert manifest["status"] == "ok" + PY diff --git a/.github/workflows/fast-mlsirm-codeql.yml b/.github/workflows/fast-mlsirm-codeql.yml new file mode 100644 index 0000000000..29f132a3af --- /dev/null +++ b/.github/workflows/fast-mlsirm-codeql.yml @@ -0,0 +1,63 @@ +name: CodeQL + +on: + workflow_call: + +permissions: + contents: read + +jobs: + analyze-actions: + # Branch protection requires a deterministic Actions-language CodeQL check + # on PRs. Default setup remains the repository's code-scanning upload owner. + name: Analyze (actions) + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd + with: + languages: actions + - uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd + with: + upload: never + output: codeql-results/actions + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: codeql-diagnostic-actions-${{ github.sha }} + path: codeql-results/actions/*.sarif + if-no-files-found: error + + analyze-python: + # Python remains a no-upload diagnostic for explicit operator runs; + # the current default setup is configured for Actions only. + if: github.event_name == 'workflow_dispatch' + name: Analyze (python) + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd + with: + languages: python + - uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd + with: + upload: never + output: codeql-results/python + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: codeql-diagnostic-python-${{ github.sha }} + path: codeql-results/python/*.sarif + if-no-files-found: error diff --git a/.github/workflows/fast-mlsirm-hourly-pr-governance.yml b/.github/workflows/fast-mlsirm-hourly-pr-governance.yml new file mode 100644 index 0000000000..f72bc91bfc --- /dev/null +++ b/.github/workflows/fast-mlsirm-hourly-pr-governance.yml @@ -0,0 +1,163 @@ +name: Hourly PR Queue Governance + +on: + workflow_call: + +permissions: + actions: read + contents: read + pull-requests: read + +concurrency: + group: central-fast-mlsirm-hourly-pr-governance-hourly-pr-governance-${{ github.repository }} + cancel-in-progress: true + +jobs: + governance-evidence: + name: Hourly PR queue governance evidence + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 10 + steps: + - name: Check out reviewed source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: "3.12" + - name: Verify the workflow contract without third-party dependencies + run: | + python - <<'PY' + import runpy + + namespace = runpy.run_path("tests/test_hourly_pr_governance_workflow.py") + test_names = [ + name + for name in sorted(namespace) + if name.startswith("test_hourly_governance_workflow_") + ] + if not test_names: + raise RuntimeError("no hourly governance contract tests were discovered") + for name in test_names: + namespace[name]() + PY + - name: Build live PR queue governance evidence + id: governance + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + is_retryable_github_failure() { + python - <<'PY' + import json + from pathlib import Path + import re + + path = Path("hourly-pr-queue-governance/pr_queue_governance_manifest.json") + if not path.is_file(): + print("false") + raise SystemExit + try: + manifest = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError): + print("false") + raise SystemExit + + status_failed = manifest.get("status") == "failed" + if not status_failed: + print("false") + raise SystemExit + + failed_checks = manifest.get("failed_checks") + retryable_failed_check_names = {"github:snapshot", "github:base_sha"} + if ( + not isinstance(failed_checks, list) + or not failed_checks + or not all( + isinstance(check, dict) and check.get("ok") is False + for check in failed_checks + ) + ): + print("false") + raise SystemExit + failed_check_names = { + str(check.get("name", "")) for check in failed_checks + } + only_transient_snapshot_checks_failed = ( + bool(failed_check_names) + and failed_check_names.issubset(retryable_failed_check_names) + ) + + github = manifest.get("github", {}) + errors = github.get("errors", []) if isinstance(github, dict) else [] + retry_status = re.compile(r"\bHTTP (?:502|503|504)\b", re.IGNORECASE) + retryable = ( + only_transient_snapshot_checks_failed + and bool(errors) + and all( + isinstance(error, dict) + and type(error.get("returncode")) is int + and error.get("returncode") != 0 + and isinstance(error.get("stderr"), str) + and retry_status.search(error.get("stderr", "")) is not None + for error in errors + ) + ) + print("true" if retryable else "false") + PY + } + + max_attempts=3 + attempt=1 + while true; do + rm -rf hourly-pr-queue-governance + mkdir -p hourly-pr-queue-governance + + snapshot_rc=0 + python scripts/capture_pr_queue_snapshot.py \ + --repo ContextualWisdomLab/fast-mlsirm \ + --out hourly-pr-queue-governance/github_snapshot.json \ + || snapshot_rc=$? + + builder_rc=0 + python scripts/build_pr_queue_governance.py \ + --repo ContextualWisdomLab/fast-mlsirm \ + --out hourly-pr-queue-governance \ + --offline-snapshot hourly-pr-queue-governance/github_snapshot.json \ + || builder_rc=$? + + if [[ "$snapshot_rc" -eq 0 && "$builder_rc" -eq 0 ]]; then + break + fi + + retryable="$(is_retryable_github_failure)" + if [[ "$retryable" != "true" || "$attempt" -ge "$max_attempts" ]]; then + exit 1 + fi + delay=$((10 * (2 ** (attempt - 1)))) + echo "::warning::Transient GitHub API failure on attempt ${attempt}; retrying in ${delay}s." + sleep "$delay" + attempt=$((attempt + 1)) + done + - name: Build live workflow registry drift evidence + env: + GH_TOKEN: ${{ github.token }} + run: | + python scripts/audit_workflow_registry.py \ + --repo ContextualWisdomLab/fast-mlsirm \ + --out hourly-pr-queue-governance/workflow_registry_audit.json + - name: Publish hourly governance evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: hourly-pr-queue-governance-${{ github.run_id }} + path: | + hourly-pr-queue-governance/github_snapshot.json + hourly-pr-queue-governance/pr_queue_governance_manifest.json + hourly-pr-queue-governance/pr_queue_governance_report.html + hourly-pr-queue-governance/workflow_registry_audit.json + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/fast-mlsirm-pypi-gap-guard.yml b/.github/workflows/fast-mlsirm-pypi-gap-guard.yml new file mode 100644 index 0000000000..c21e261037 --- /dev/null +++ b/.github/workflows/fast-mlsirm-pypi-gap-guard.yml @@ -0,0 +1,166 @@ +name: PyPI Gap Guard + +# Idempotent recovery: when a GitHub Release exists for a canonical vX.Y.Z tag +# whose project version matches pyproject.toml at that tag, but the version is +# absent from PyPI, dispatch the local publish-pypi.yml control plane with the +# same provenance inputs as release-tag (release_tag, release_commit from the +# tag, control_plane_commit = default-branch HEAD). Never retargets tags. +# See docs/orchestration/release-publish-contract.md and ADR-0029. + +on: + workflow_call: + +permissions: + contents: read + +concurrency: + group: central-fast-mlsirm-pypi-gap-guard-pypi-gap-guard + cancel-in-progress: false + +jobs: + recover-missing-pypi: + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 15 + permissions: + contents: read + actions: write + steps: + - name: Checkout default-branch control plane + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + fetch-depth: 0 + fetch-tags: true + ref: ${{ github.event.repository.default_branch }} + + - name: Resolve control-plane commit and PyPI inventory + id: inventory + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + EVENT_NAME: ${{ github.event_name }} + RELEASE_TAG_HINT: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + CONTROL_PLANE_COMMIT="$(git rev-parse HEAD)" + if ! printf '%s' "$CONTROL_PLANE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "default-branch HEAD is unavailable for package publication dispatch" >&2 + exit 1 + fi + echo "control_plane_commit=$CONTROL_PLANE_COMMIT" >> "$GITHUB_OUTPUT" + + pypi_json="$(mktemp)" + cleanup() { + rm -f "$pypi_json" + } + trap cleanup EXIT + + # Prefer the public PyPI JSON API over GitHub API spam for package + # version inventory. + curl --silent --show-error --fail \ + --header "Accept: application/json" \ + --output "$pypi_json" \ + "https://pypi.org/pypi/fast-mlsirm/json" + + python3 - "$pypi_json" <<'PY' + import json + import os + import sys + from pathlib import Path + + payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + releases = payload.get("releases") or {} + versions = sorted(str(name) for name in releases.keys()) + Path("pypi_versions.txt").write_text( + "\n".join(versions) + ("\n" if versions else ""), + encoding="utf-8", + ) + print(f"loaded {len(versions)} PyPI versions for fast-mlsirm") + PY + + candidate_tags=() + if [ "$EVENT_NAME" = "release" ] && [ -n "${RELEASE_TAG_HINT:-}" ]; then + candidate_tags+=("$RELEASE_TAG_HINT") + else + # gh release list is scoped to this repository and avoids a broad + # REST pagination loop for every org repo. + while IFS= read -r tag; do + [ -n "$tag" ] || continue + candidate_tags+=("$tag") + done < <( + gh release list --repo "$GITHUB_REPOSITORY" --limit 100 \ + --json tagName \ + --jq '.[].tagName' + ) + fi + + : > gap_tags.txt + for tag in "${candidate_tags[@]}"; do + if ! printf '%s' "$tag" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then + echo "skipping non-canonical release tag: $tag" + continue + fi + version="${tag#v}" + if grep -Fxq "$version" pypi_versions.txt; then + echo "PyPI already has $version; skipping $tag" + continue + fi + printf '%s\n' "$tag" >> gap_tags.txt + done + + if [ ! -s gap_tags.txt ]; then + echo "no PyPI gaps detected" + echo "gap_count=0" >> "$GITHUB_OUTPUT" + else + echo "gap_count=$(wc -l < gap_tags.txt | tr -d ' ')" >> "$GITHUB_OUTPUT" + echo "detected gaps:" + cat gap_tags.txt + fi + + - name: Dispatch publish-pypi for each missing version + if: steps.inventory.outputs.gap_count != '0' + env: + GH_TOKEN: ${{ github.token }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + CONTROL_PLANE_COMMIT: ${{ steps.inventory.outputs.control_plane_commit }} + run: | + set -euo pipefail + while IFS= read -r release_tag; do + [ -n "$release_tag" ] || continue + if ! release_commit="$(git rev-parse "$release_tag^{commit}" 2>/dev/null)"; then + echo "release tag $release_tag is unavailable for provenance verification" >&2 + exit 1 + fi + if ! printf '%s' "$release_commit" | grep -Eq '^[0-9a-f]{40}$'; then + echo "release commit for $release_tag is not a canonical SHA-1" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$release_commit" "$CONTROL_PLANE_COMMIT"; then + echo "release commit for $release_tag must be an ancestor of default-branch HEAD" >&2 + exit 1 + fi + + project_version="$( + git show "$release_commit:pyproject.toml" | python3 -c ' + import sys + import tomllib + + project = tomllib.loads(sys.stdin.read())["project"] + print(project["version"]) + ' + )" + expected_tag="v$project_version" + if [ "$release_tag" != "$expected_tag" ]; then + echo "tag $release_tag does not match pyproject version $project_version at $release_commit" >&2 + exit 1 + fi + + echo "dispatching publish-pypi.yml for $release_tag @ $release_commit (control plane $CONTROL_PLANE_COMMIT)" + gh workflow run publish-pypi.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref "$DEFAULT_BRANCH" \ + -f release_tag="$release_tag" \ + -f release_commit="$release_commit" \ + -f control_plane_commit="$CONTROL_PLANE_COMMIT" + done < gap_tags.txt diff --git a/.github/workflows/fast-mlsirm-statistical-studies.yml b/.github/workflows/fast-mlsirm-statistical-studies.yml new file mode 100644 index 0000000000..276c98f8d7 --- /dev/null +++ b/.github/workflows/fast-mlsirm-statistical-studies.yml @@ -0,0 +1,170 @@ +name: Statistical Studies + +on: + workflow_call: + +permissions: + contents: read + +# Secret boundary: this evidence workflow intentionally consumes no secret +# inputs. If a future study needs credentials, pass them only through an +# explicitly reviewed `${{ secrets.NAME }}` value in `env`; never hardcode, +# interpolate into command arguments, or print a credential. + +concurrency: + group: central-fast-mlsirm-statistical-studies-statistical-studies-${{ github.ref }} + cancel-in-progress: false + +jobs: + rust-ignored: + name: Ignored Rust studies shard ${{ matrix.shard }} + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + # Large Monte Carlo recovery tests are real evidence, not a reason to + # silently discard a shard after the helper's default 30-minute bound. + timeout-minutes: 180 + env: + FAST_MLSIRM_STATISTICAL_TEST_TIMEOUT_SECONDS: "7200" + strategy: + fail-fast: false + max-parallel: 3 + matrix: + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: "3.12" + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.97.1 + - name: Run target-qualified ignored Rust shard + run: >- + python scripts/run_ignored_rust_shard.py + --shard ${{ matrix.shard }} + --shards 12 + --skip mlsirm-core/test/literature_true_parameter_recovery::kang_jeon_2025_minimum_cell_recovers_true_parameters + --skip mlsirm-core/test/literature_true_parameter_recovery::gpu_recovery_matches_cpu_on_paper_design + --skip mlsirm-core/test/higher_order_mc_recovery::higher_order_dina_recovery_respects_monte_carlo_tolerance + --skip mlsirm-core/lib/mlsirm_core::grm::tests::mc_grm_recovery_500 + + rust-pyo3-ignored: + name: Ignored PyO3 studies + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.97.1 + - run: cargo test --locked --release --manifest-path crates/fast-mlsirm-py/Cargo.toml -- --ignored --test-threads=1 --nocapture + + rust-recovery: + name: Literature CPU recovery studies + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 120 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.97.1 + - name: Run Kang-Jeon paper-design recovery + run: | + set -euo pipefail + # cargo test --exact exits 0 when the filter matches no test, so + # require exactly one executed passing test (fail-closed, #1869). + cargo test --locked --release -p mlsirm-core \ + --test literature_true_parameter_recovery \ + kang_jeon_2025_minimum_cell_recovers_true_parameters \ + -- --ignored --exact --nocapture 2>&1 | tee kang-jeon-recovery.log + grep -q "test result: ok. 1 passed" kang-jeon-recovery.log + - name: Run higher-order DINA Monte Carlo recovery + run: | + set -euo pipefail + # cargo test --exact exits 0 when the filter matches no test, so + # require exactly one executed passing test (fail-closed, #1869). + cargo test --locked --release -p mlsirm-core \ + --test higher_order_mc_recovery \ + higher_order_dina_recovery_respects_monte_carlo_tolerance \ + -- --ignored --exact --nocapture 2>&1 | tee higher-order-dina-recovery.log + grep -q "test result: ok. 1 passed" higher-order-dina-recovery.log + + grm-recovery: + name: GRM 500-rep recovery study + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 120 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.97.1 + - name: Run multidimensional GRM Monte Carlo recovery + run: | + set -euo pipefail + cargo test --locked --release -p mlsirm-core --lib \ + grm::tests::mc_grm_recovery_500 \ + -- --ignored --exact --nocapture --test-threads=1 \ + 2>&1 | tee grm-recovery-study.log + # cargo test --exact exits 0 when the filter matches no test, so + # require exactly one executed passing test (fail-closed, #1869). + grep -q "test result: ok. 1 passed" grm-recovery-study.log + - name: Publish GRM recovery study log + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: grm-recovery-study-${{ github.run_id }} + path: grm-recovery-study.log + if-no-files-found: error + retention-days: 90 + + gpu-recovery: + name: Literature GPU recovery parity + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 90 + env: + WGPU_BACKEND: vulkan + XDG_RUNTIME_DIR: /tmp/fast-mlsirm-xdg-runtime + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: 1.97.1 + - name: Install software Vulkan adapter + run: | + sudo apt-get update + sudo apt-get install --yes mesa-vulkan-drivers vulkan-tools + mkdir -p "$XDG_RUNTIME_DIR" + chmod 700 "$XDG_RUNTIME_DIR" + LVP_ICD="$(find /usr/share/vulkan/icd.d -maxdepth 1 -type f -name '*lvp*.json' -print -quit)" + test -n "$LVP_ICD" + echo "VK_ICD_FILENAMES=$LVP_ICD" >> "$GITHUB_ENV" + - run: vulkaninfo --summary + - name: Run paper-design GPU recovery parity + run: | + set -euo pipefail + # cargo test --exact exits 0 when the filter matches no test, so + # require exactly one executed passing test (fail-closed, #1869). + cargo test --locked --release -p mlsirm-core \ + --test literature_true_parameter_recovery \ + gpu_recovery_matches_cpu_on_paper_design \ + -- --ignored --exact --nocapture 2>&1 | tee gpu-recovery-parity.log + grep -q "test result: ok. 1 passed" gpu-recovery-parity.log From 717389018391ebf67970f5764ba008d36bf042a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:06:26 +0900 Subject: [PATCH 02/16] test(ci): preserve fast-mlsirm gates and isolated runner contracts --- .../test_fast_mlsirm_self_hosted_workflows.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_fast_mlsirm_self_hosted_workflows.py diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py new file mode 100644 index 0000000000..35907f3949 --- /dev/null +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -0,0 +1,18 @@ +from pathlib import Path + + +def test_fast_mlsirm_reusable_workflows_keep_isolated_runner_and_gate_contracts(): + root = Path(__file__).resolve().parents[1] + workflows = root / ".github" / "workflows" + for name in ("ci", "codeql", "statistical-studies", "hourly-pr-governance", "pypi-gap-guard"): + text = (workflows / f"fast-mlsirm-{name}.yml").read_text() + assert "workflow_call:" in text + assert "ubuntu-latest" not in text and "runs-on: ubuntu" not in text + assert "group: CWL CI isolated" in text + assert "labels: [self-hosted, Linux, X64]" in text + assert f"group: central-fast-mlsirm-{name}-" in text + ci = (workflows / "fast-mlsirm-ci.yml").read_text() + assert 'python-version: ["3.12", "3.14"]' in ci + assert 'test "${{ needs.python-matrix.result }}" = "success"' in ci + assert "cargo test --locked --workspace" in ci + assert "cargo test --locked --manifest-path crates/fast-mlsirm-py/Cargo.toml" in ci From b97821885a08d9dae556da34b730677d14ee8c97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:07:16 +0900 Subject: [PATCH 03/16] ci(codeql): coalesce reusable scans before runner admission --- .github/workflows/fast-mlsirm-codeql.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/fast-mlsirm-codeql.yml b/.github/workflows/fast-mlsirm-codeql.yml index 29f132a3af..a799a9191a 100644 --- a/.github/workflows/fast-mlsirm-codeql.yml +++ b/.github/workflows/fast-mlsirm-codeql.yml @@ -3,6 +3,10 @@ name: CodeQL on: workflow_call: +concurrency: + group: central-fast-mlsirm-codeql-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read From 64780546e0dccae26aea0482037c879535ac1657 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:10:50 +0900 Subject: [PATCH 04/16] ci(noema): use LiteLLM auto for public reviews --- .github/workflows/noema-review.yml | 42 +++++++++++++++---- .../test_fast_mlsirm_self_hosted_workflows.py | 10 +++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 08ea600538..6baa4e1ff9 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -853,9 +853,24 @@ jobs: python-version: "3.12" update-environment: false + - name: Select public review gateway + id: llm_gateway + if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' + env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + REQUIRE_ZDR: ${{ steps.target_visibility.outputs.require_zdr }} + run: | + set -euo pipefail + # Private targets retain the verified ZDR sidecar route. + if [ "$REQUIRE_ZDR" = "false" ] && [ -n "${LLM_GATEWAY_API_KEY:-}" ]; then + echo "enabled=true" >>"$GITHUB_OUTPUT" + else + echo "enabled=false" >>"$GITHUB_OUTPUT" + fi + - name: Provision contextual-orchestrator review sidecar id: noema_sidecar - if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' + if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' && steps.llm_gateway.outputs.enabled != 'true' env: SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} @@ -912,6 +927,8 @@ jobs: if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' id: noema_prepare env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + USE_LLM_GATEWAY: ${{ steps.llm_gateway.outputs.enabled }} GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app' || 'noema-review-app-oidc' }} NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_token.outputs['app-slug']) || '' }} @@ -928,15 +945,22 @@ jobs: echo "::error::Noema reviewer credential selection succeeded but no token was minted; review cannot prepare a verdict." exit 1 fi - if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-}" ]; then - echo "::error::contextual-orchestrator review sidecar must be provisioned before Noema LLM review." - exit 1 + if [ "${USE_LLM_GATEWAY:-false}" = "true" ]; then + test -n "${LLM_GATEWAY_API_KEY:-}" + export NOEMA_LLM_API_URL="https://litellm.poinnetworks.net/v1/chat/completions" + export NOEMA_LLM_MODEL="auto" + export NOEMA_LLM_API_KEY="$LLM_GATEWAY_API_KEY" + else + if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-}" ]; then + echo "::error::A verified review route must be provisioned before Noema LLM review." + exit 1 + fi + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + export NOEMA_LLM_API_URL="${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/v1/chat/completions" + export NOEMA_LLM_MODEL="orchestrator/free" + export NOEMA_LLM_API_KEY="${CONTEXTUAL_ORCHESTRATOR_TOKEN}" + export NOEMA_LLM_VIA_ORCHESTRATOR=1 fi - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" - export NOEMA_LLM_API_URL="${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/v1/chat/completions" - export NOEMA_LLM_MODEL="orchestrator/free" - export NOEMA_LLM_API_KEY="${CONTEXTUAL_ORCHESTRATOR_TOKEN}" - export NOEMA_LLM_VIA_ORCHESTRATOR=1 verdict_file="${RUNNER_TEMP}/noema-verdict-envelope.json" rm -f "$verdict_file" python3 "$GITHUB_WORKSPACE/.github/actions/noema-review/two_phase.py" --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" --expected-head "$EXPECTED_HEAD_SHA" --prepare-verdict-file "$verdict_file" diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py index 35907f3949..439b060ad0 100644 --- a/tests/test_fast_mlsirm_self_hosted_workflows.py +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -16,3 +16,13 @@ def test_fast_mlsirm_reusable_workflows_keep_isolated_runner_and_gate_contracts( assert 'test "${{ needs.python-matrix.result }}" = "success"' in ci assert "cargo test --locked --workspace" in ci assert "cargo test --locked --manifest-path crates/fast-mlsirm-py/Cargo.toml" in ci + + +def test_noema_uses_litellm_auto_for_public_reviews_and_keeps_private_sidecar(): + root = Path(__file__).resolve().parents[1] + text = (root / ".github" / "workflows" / "noema-review.yml").read_text() + assert "https://litellm.poinnetworks.net/v1/chat/completions" in text + assert 'export NOEMA_LLM_MODEL="auto"' in text + assert "secrets.LLM_GATEWAY_API_KEY" in text + assert "Private targets retain the verified ZDR sidecar route." in text + assert "steps.llm_gateway.outputs.enabled != 'true'" in text From 807011a5e0e2f4efd5029d90f405edd6332b8b3b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:16:14 +0900 Subject: [PATCH 05/16] ci(opencode): use LiteLLM auto for public reviews --- .../workflows/opencode-review-dispatch.yml | 49 +++++++++++++++++-- .../test_fast_mlsirm_self_hosted_workflows.py | 36 ++++++++++++++ 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 09bbf8181a..1c3514d232 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -2655,7 +2655,21 @@ jobs: python-version: "3.12" update-environment: false + - name: Select public review gateway + id: llm_gateway + env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + IS_PRIVATE: ${{ needs.validate-pr-metadata.outputs.is_private }} + run: | + set -euo pipefail + if [ "$IS_PRIVATE" = "false" ] && [ -n "${LLM_GATEWAY_API_KEY:-}" ]; then + echo "enabled=true" >>"$GITHUB_OUTPUT" + else + echo "enabled=false" >>"$GITHUB_OUTPUT" + fi + - name: Provision contextual-orchestrator review sidecar + if: steps.llm_gateway.outputs.enabled != 'true' env: SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} @@ -3752,6 +3766,7 @@ jobs: - name: Prepare isolated OpenCode review workspace env: + USE_LLM_GATEWAY: ${{ steps.llm_gateway.outputs.enabled }} OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md @@ -4224,6 +4239,22 @@ jobs: echo '::error::Generated isolated opencode.jsonc is missing the contextual-orchestrator gateway route.' exit 1 fi + if [ "${USE_LLM_GATEWAY:-false}" = "true" ]; then + jq ' + .model = "contextual-orchestrator/auto" | + .small_model = "contextual-orchestrator/auto" | + .provider["contextual-orchestrator"].name = "LiteLLM" | + .provider["contextual-orchestrator"].options = { + "baseURL": "https://litellm.poinnetworks.net/v1", + "apiKey": "{env:LLM_GATEWAY_API_KEY}" + } | + .provider["contextual-orchestrator"].models = { + "auto": .provider["contextual-orchestrator"].models["orchestrator/free"] + } | + .provider["contextual-orchestrator"].models.auto.name = "Auto" + ' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" >"$gateway_config" + mv -- "$gateway_config" "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" + fi printf 'Prepared isolated OpenCode review workspace: %s\n' "$OPENCODE_REVIEW_WORKDIR" - name: Run OpenCode PR Review model pool @@ -4231,10 +4262,12 @@ jobs: if: needs.coverage-evidence.result == 'success' continue-on-error: true env: + USE_LLM_GATEWAY: ${{ steps.llm_gateway.outputs.enabled }} + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/free" + OPENCODE_MODEL_CANDIDATES: ${{ steps.llm_gateway.outputs.enabled == 'true' && 'contextual-orchestrator/auto' || 'contextual-orchestrator/orchestrator/free' }} # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. @@ -4272,7 +4305,9 @@ jobs: RUN_ATTEMPT: ${{ github.run_attempt }} run: | set -euo pipefail - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + if [ "${USE_LLM_GATEWAY:-false}" != "true" ]; then + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + fi set +e bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh" pool_status=$? @@ -4830,6 +4865,8 @@ jobs: # extensions for image validation or package/GPU builds plus API and # publication overhead. env: + USE_LLM_GATEWAY: ${{ steps.llm_gateway.outputs.enabled }} + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} CHECK_LOOKUP_GH_TOKEN: ${{ github.token }} # The OpenCode app installation token is exchanged from api.opencode.ai @@ -4852,7 +4889,7 @@ jobs: COVERAGE_EVIDENCE_SUMMARY: ${{ needs.coverage-evidence.outputs.coverage_summary || 'Coverage evidence job did not run or did not publish coverage evidence.' }} OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head - MODEL: contextual-orchestrator/orchestrator/free + MODEL: ${{ steps.llm_gateway.outputs.enabled == 'true' && 'contextual-orchestrator/auto' || 'contextual-orchestrator/orchestrator/free' }} USE_GITHUB_TOKEN: "true" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" @@ -6214,8 +6251,10 @@ jobs: printf 'Skipping publish-step failed-check OpenCode diagnosis for central review-process self-repair; using collected current-head failed-check logs/SARIF fallback so the publish step stays bounded.\n' >&2 return 1 fi - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" - if [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then + if [ "${USE_LLM_GATEWAY:-false}" != "true" ]; then + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + fi + if [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}${LLM_GATEWAY_API_KEY:-}" ]; then return 1 fi if ! python3 "$GITHUB_WORKSPACE/scripts/ci/assert_opencode_reasoning_effort.py" \ diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py index 439b060ad0..e380c81253 100644 --- a/tests/test_fast_mlsirm_self_hosted_workflows.py +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -26,3 +26,39 @@ def test_noema_uses_litellm_auto_for_public_reviews_and_keeps_private_sidecar(): assert "secrets.LLM_GATEWAY_API_KEY" in text assert "Private targets retain the verified ZDR sidecar route." in text assert "steps.llm_gateway.outputs.enabled != 'true'" in text + + +def test_public_gateway_selection_rejects_private_unknown_and_missing_credentials(tmp_path): + import os + import subprocess + import textwrap + + root = Path(__file__).resolve().parents[1] + for filename, flag in (("noema-review.yml", "REQUIRE_ZDR"), ("opencode-review-dispatch.yml", "IS_PRIVATE")): + text = (root / ".github" / "workflows" / filename).read_text() + step = text.split(" - name: Select public review gateway\n", 1)[1].split("\n - name:", 1)[0] + command = textwrap.dedent(step.split(" run: |\n", 1)[1]) + for visibility, key, enabled in (("false", "test-placeholder", True), ("true", "test-placeholder", False), ("", "test-placeholder", False), ("false", "", False)): + output = tmp_path / "output" + output.write_text("") + env = {**os.environ, flag: visibility, "LLM_GATEWAY_API_KEY": key, "GITHUB_OUTPUT": str(output)} + subprocess.run(["bash", "-c", command], env=env, check=True, capture_output=True) + assert output.read_text() == f"enabled={str(enabled).lower()}\n" + + +def test_opencode_gateway_config_keeps_reasoning_and_tools(): + import json + import subprocess + + root = Path(__file__).resolve().parents[1] + text = (root / ".github" / "workflows" / "opencode-review-dispatch.yml").read_text() + start = text.index(' jq \'\n .model = "contextual-orchestrator/auto"') + jq_filter = text[start:].split("jq '", 1)[1].split("' ", 1)[0] + original = {"provider": {"contextual-orchestrator": {"models": {"orchestrator/free": {"tool_call": True, "reasoning": True, "options": {"reasoningEffort": "high"}}}}}} + completed = subprocess.run(["jq", jq_filter], input=json.dumps(original), text=True, capture_output=True, check=True) + config = json.loads(completed.stdout) + assert config["model"] == "contextual-orchestrator/auto" + provider = config["provider"]["contextual-orchestrator"] + assert provider["options"] == {"baseURL": "https://litellm.poinnetworks.net/v1", "apiKey": "{env:LLM_GATEWAY_API_KEY}"} + assert provider["models"]["auto"]["tool_call"] + assert provider["models"]["auto"]["options"]["reasoningEffort"] == "high" From deb86d0a96a9478319dd015a902131d698fa79e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:19:00 +0900 Subject: [PATCH 06/16] ci: centralize release control and remove fast-mlsirm hosted scanner admission --- .github/workflows/fast-mlsirm-cflite_pr.yml | 47 + .../workflows/fast-mlsirm-publish-pypi.yml | 1950 +++++++++++++++++ .github/workflows/fast-mlsirm-release-tag.yml | 343 +++ .github/workflows/sast-semgrep.yml | 2 +- .github/workflows/security-scan.yml | 12 +- .../test_fast_mlsirm_self_hosted_workflows.py | 13 + 6 files changed, 2360 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/fast-mlsirm-cflite_pr.yml create mode 100644 .github/workflows/fast-mlsirm-publish-pypi.yml create mode 100644 .github/workflows/fast-mlsirm-release-tag.yml diff --git a/.github/workflows/fast-mlsirm-cflite_pr.yml b/.github/workflows/fast-mlsirm-cflite_pr.yml new file mode 100644 index 0000000000..63482da31a --- /dev/null +++ b/.github/workflows/fast-mlsirm-cflite_pr.yml @@ -0,0 +1,47 @@ +name: ClusterFuzzLite PR fuzzing + +on: + workflow_call: + +permissions: + contents: read + +jobs: + PR: + if: ${{ !github.event.pull_request.draft && github.event.action != 'closed' }} + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + permissions: + contents: read + security-events: write + concurrency: + group: central-fast-mlsirm-cflite-${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + sanitizer: [address] + steps: + - name: Prepare ClusterFuzzLite output directory + run: | + mkdir -p "$GITHUB_WORKSPACE/build-out" + chmod -R a+rwx "$GITHUB_WORKSPACE/build-out" + echo "Prepared ClusterFuzzLite build-out for non-root fuzzbuilder writes" + ls -ld "$GITHUB_WORKSPACE" "$GITHUB_WORKSPACE/build-out" + - name: Build fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 + with: + language: rust + github-token: ${{ secrets.GITHUB_TOKEN }} + sanitizer: ${{ matrix.sanitizer }} + - name: Run fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 120 + mode: code-change + output-sarif: true + sanitizer: ${{ matrix.sanitizer }} diff --git a/.github/workflows/fast-mlsirm-publish-pypi.yml b/.github/workflows/fast-mlsirm-publish-pypi.yml new file mode 100644 index 0000000000..2c44cf87b7 --- /dev/null +++ b/.github/workflows/fast-mlsirm-publish-pypi.yml @@ -0,0 +1,1950 @@ +name: Publish Package +on: + workflow_call: + inputs: + release_tag: + description: "Immutable release tag to publish (for example v0.9.0)" + required: true + type: string + release_commit: + description: "Reviewed release source commit (full lowercase SHA-1)" + required: true + type: string + control_plane_commit: + description: "Protected default-branch commit that selected this publication workflow" + required: true + type: string + +permissions: + contents: read + +concurrency: + group: central-fast-mlsirm-publish-pypi-publish-pypi-${{ inputs.release_tag }} + cancel-in-progress: false + +jobs: + verify-release: + name: verify release provenance + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 5 + outputs: + release_commit: ${{ steps.release-source.outputs.value }} + source_date_epoch: ${{ steps.source-date-epoch.outputs.value }} + steps: + - name: Verify publication control-plane identity + env: + CONTROL_PLANE_COMMIT: ${{ inputs.control_plane_commit }} + CONTROL_PLANE_SHA: ${{ github.sha }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + PUBLISH_REF: ${{ github.ref }} + run: | + set -euo pipefail + if ! printf '%s' "$CONTROL_PLANE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "control_plane_commit must be a canonical 40-character lowercase SHA-1" >&2 + exit 1 + fi + expected_ref="refs/heads/$DEFAULT_BRANCH" + if [ "$PUBLISH_REF" != "$expected_ref" ]; then + echo "package publication must run from $expected_ref, not $PUBLISH_REF" >&2 + exit 1 + fi + if [ "$CONTROL_PLANE_SHA" != "$CONTROL_PLANE_COMMIT" ]; then + echo "publication control plane moved after release verification" >&2 + exit 1 + fi + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + - name: Validate and select release source ancestry + id: release-source + env: + CONTROL_PLANE_COMMIT: ${{ inputs.control_plane_commit }} + RELEASE_COMMIT: ${{ inputs.release_commit }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if ! printf '%s' "$RELEASE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "release_commit must be a canonical 40-character lowercase SHA-1" >&2 + exit 1 + fi + trusted_control_commit="$(git rev-parse HEAD)" + if [ "$trusted_control_commit" != "$CONTROL_PLANE_COMMIT" ]; then + echo "checked-out protected control plane does not match control_plane_commit" >&2 + exit 1 + fi + canonical_release_commit="$(git rev-parse --verify "$RELEASE_COMMIT^{commit}" 2>/dev/null)" || { + echo "release_commit is unavailable for ancestry verification" >&2 + exit 1 + } + if [ "$canonical_release_commit" != "$RELEASE_COMMIT" ]; then + echo "release_commit did not resolve to its canonical commit identity" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$canonical_release_commit" "$trusted_control_commit"; then + echo "release commit must be an ancestor of the publication control plane" >&2 + exit 1 + fi + echo "value=$canonical_release_commit" >> "$GITHUB_OUTPUT" + git checkout --detach "$canonical_release_commit" + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: "3.12" + - name: Require release tag and source commit to match project version + env: + RELEASE_COMMIT: ${{ steps.release-source.outputs.value }} + RELEASE_TAG: ${{ inputs.release_tag }} + run: | + set -euo pipefail + checked_out_commit="$(git rev-parse HEAD)" + if [ "$checked_out_commit" != "$RELEASE_COMMIT" ]; then + echo "checked-out release source does not match release_commit" >&2 + exit 1 + fi + # R5a: the immutable tag is created only after release admission + # (create-tag-and-release). A missing tag is the normal first-run + # state; an existing tag (a retry) must already target release_commit. + if tag_commit="$(git rev-parse -q --verify "$RELEASE_TAG^{commit}" 2>/dev/null)"; then + if [ "$tag_commit" != "$RELEASE_COMMIT" ]; then + echo "release tag does not target release_commit" >&2 + exit 1 + fi + else + echo "release tag $RELEASE_TAG does not exist yet; it is created only after release admission" + fi + python - <<'PY' + import os + import tomllib + + with open("pyproject.toml", "rb") as stream: + project = tomllib.load(stream)["project"] + + expected = f"v{project['version']}" + actual = os.environ["RELEASE_TAG"] + if actual != expected: + raise SystemExit( + f"release tag does not match project version: expected {expected!r}, got {actual!r}" + ) + PY + # Reproducible-build clock: the committer time of the exact release + # commit, never the wall clock. maturin stamps it into the embedded + # CycloneDX SBOM (timestamp/serialNumber) and archive metadata; without it + # two builds of the same commit produce different wheel bytes. + - name: Derive SOURCE_DATE_EPOCH from the release commit + id: source-date-epoch + env: + RELEASE_COMMIT: ${{ steps.release-source.outputs.value }} + run: | + set -euo pipefail + epoch="$(git log -1 --format=%ct "$RELEASE_COMMIT")" + if ! printf '%s' "$epoch" | grep -Eq '^[0-9]+$'; then + echo "could not derive SOURCE_DATE_EPOCH from release_commit" >&2 + exit 1 + fi + echo "value=$epoch" >> "$GITHUB_OUTPUT" + echo "SOURCE_DATE_EPOCH=$epoch (committer time of $RELEASE_COMMIT)" + + sdist: + name: sdist + needs: verify-release + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 15 + env: + SOURCE_DATE_EPOCH: ${{ needs.verify-release.outputs.source_date_epoch }} + CARGO_RELEASE_SHA: ${{ needs.verify-release.outputs.release_commit }} + CARGO_RELEASE_LEG: sdist + CARGO_BUILD_IMAGE: "" + CARGO_BUILD_PYTHON: python + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + - name: Validate release ancestry in this job + env: + CONTROL_PLANE_COMMIT: ${{ github.sha }} + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if ! printf '%s' "$RELEASE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "release_commit must be a canonical 40-character lowercase SHA-1" >&2 + exit 1 + fi + trusted_control_commit="$(git rev-parse HEAD)" + if [ "$trusted_control_commit" != "$CONTROL_PLANE_COMMIT" ]; then + echo "checked-out protected control plane does not match control_plane_commit" >&2 + exit 1 + fi + canonical_release_commit="$(git rev-parse --verify "$RELEASE_COMMIT^{commit}" 2>/dev/null)" || { + echo "release_commit is unavailable for ancestry verification" >&2 + exit 1 + } + if [ "$canonical_release_commit" != "$RELEASE_COMMIT" ]; then + echo "release_commit did not resolve to its canonical commit identity" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$canonical_release_commit" "$trusted_control_commit"; then + echo "release commit must be an ancestor of the publication control plane" >&2 + exit 1 + fi + git checkout --detach "$canonical_release_commit" + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: "3.12" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + path: trusted-control + persist-credentials: false + - name: Build sdist + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b + with: + command: sdist + maturin-version: v1.15.0 + args: --out dist + - name: Capture first sdist build tools + env: + CARGO_BUILD_PASS: first + run: python trusted-control/scripts/ci/capture_release_build_scope.py + - name: Rebuild sdist for byte-reproducibility check + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b + with: + command: sdist + maturin-version: v1.15.0 + args: --out dist-rebuild + - name: Capture second sdist build tools + env: + CARGO_BUILD_PASS: second + run: python trusted-control/scripts/ci/capture_release_build_scope.py + # Historical release tags predate the pyproject include workaround for + # maturin omitting LICENSE from mixed-layout sdists while still writing + # License-File metadata. Inject the checked-out LICENSE when missing so + # already-cut tags remain publishable without retagging. The rewrite pins + # the gzip header (mtime = SOURCE_DATE_EPOCH, no embedded file name) and + # the injected member's metadata so it is byte-reproducible. + - name: Ensure LICENSE is present in the sdist + run: | + set -euo pipefail + python - <<'PY' + from __future__ import annotations + + import gzip + import io + import os + import tarfile + from pathlib import Path + + epoch = os.environ.get("SOURCE_DATE_EPOCH", "") + if not epoch.isdigit(): + raise SystemExit(f"SOURCE_DATE_EPOCH is not set to a release-commit epoch: {epoch!r}") + license_path = Path("LICENSE") + if not license_path.is_file(): + raise SystemExit("checked-out release source is missing LICENSE") + license_bytes = license_path.read_bytes() + + for dist in (Path("dist"), Path("dist-rebuild")): + archives = sorted(dist.glob("*.tar.gz")) + if len(archives) != 1: + raise SystemExit(f"expected exactly one sdist archive in {dist}, found {archives!r}") + archive = archives[0] + + with tarfile.open(archive, "r:gz") as reader: + names = reader.getnames() + members = reader.getmembers() + file_members = { + member.name: reader.extractfile(member).read() + for member in members + if member.isfile() + } + + roots = {name.split("/", 1)[0] for name in names if name} + if len(roots) != 1: + raise SystemExit(f"sdist must have a single top-level directory, found {sorted(roots)!r}") + root = next(iter(roots)) + license_member = f"{root}/LICENSE" + if license_member in file_members: + if file_members[license_member] != license_bytes: + raise SystemExit("sdist LICENSE content does not match checked-out LICENSE") + print(f"{archive} already contains {license_member}") + continue + + rebuilt = archive.with_suffix(archive.suffix + ".rewritten") + with ( + tarfile.open(archive, "r:gz") as reader, + open(rebuilt, "wb") as raw, + gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=int(epoch)) as compressed, + tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as writer, + ): + for member in members: + payload = reader.extractfile(member) if member.isfile() else None + writer.addfile(member, payload) + info = tarfile.TarInfo(name=license_member) + info.size = len(license_bytes) + info.mode = 0o644 + info.mtime = int(epoch) + writer.addfile(info, io.BytesIO(license_bytes)) + + rebuilt.replace(archive) + with tarfile.open(archive, "r:gz") as reader: + if license_member not in reader.getnames(): + raise SystemExit(f"failed to inject {license_member} into {archive}") + print(f"injected {license_member} into {archive}") + PY + - name: Compare double-build sdist digests and record them + shell: bash + env: + LEG: sdist + ARTIFACT_GLOB: "*.tar.gz" + CONTAINER_IMAGE: "" + run: | + set -euo pipefail + python - <<'PY' + import hashlib + import json + import os + import re + import subprocess + from pathlib import Path + + # Claim scope: a clean-target repeat build in the SAME runner and + # container environment. This is not independent-environment or + # independent-toolchain provenance. + leg = os.environ["LEG"] + pattern = os.environ["ARTIFACT_GLOB"] + + def digest(directory: str) -> tuple[str, str]: + found = sorted(Path(directory).glob(pattern)) + if len(found) != 1: + raise SystemExit(f"expected exactly one {pattern} in {directory}, found {found!r}") + return found[0].name, hashlib.sha256(found[0].read_bytes()).hexdigest() + + epoch = os.environ.get("SOURCE_DATE_EPOCH", "") + if not epoch.isdigit(): + raise SystemExit(f"SOURCE_DATE_EPOCH is not set to a release-commit epoch: {epoch!r}") + first = digest("dist") + second = digest("dist-rebuild") + if first != second: + raise SystemExit(f"{leg} is not byte-reproducible: {first!r} != {second!r}") + + # Provenance is bound to THIS invocation: Linux wheel legs build in + # the digest-pinned container passed to maturin-action, which must be + # the image present on the runner; every other row (macOS, Windows, + # sdist) ran without a container and records the runner identity. + # Unrelated cached images are never consulted. + container = os.environ.get("CONTAINER_IMAGE", "") + runner_os = os.environ.get("RUNNER_OS", "") + if runner_os == "Linux" and leg != "sdist": + if not re.fullmatch(r"[a-z0-9.\-]+(?:/[a-z0-9._\-]+)+@sha256:[0-9a-f]{64}", container): + raise SystemExit(f"{leg}: Linux wheel leg has no digest-pinned build container: {container!r}") + inspected = subprocess.run( + ["docker", "image", "inspect", "--format", "{{json .RepoDigests}}", container], + capture_output=True, text=True, + ) + if inspected.returncode != 0 or container not in json.loads(inspected.stdout or "[]"): + raise SystemExit(f"{leg}: pinned build container {container} is not present on this runner") + build_env = f"container:{container}" + else: + if container: + raise SystemExit(f"{leg}: ran without a container but carries container provenance {container!r}") + identity = [os.environ.get(key, "") for key in ("ImageOS", "ImageVersion", "RUNNER_OS", "RUNNER_ARCH")] + if not all(identity): + raise SystemExit(f"{leg}: missing runner identity (ImageOS/ImageVersion/RUNNER_OS/RUNNER_ARCH): {identity!r}") + build_env = "runner:" + "/".join(identity) + + name, sha = first + out = Path("repro-digest") + out.mkdir(exist_ok=True) + row = "\t".join([leg, "true", "clean-target-repeat-same-env", sha, second[1], name, build_env]) + (out / f"{leg}.tsv").write_text(row + "\n", encoding="utf-8") + print(row) + PY + - name: Capture finished sdist members from the build leg + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + run: python trusted-control/scripts/ci/capture_release_bundle.py repro-digest/sdist.tsv dist + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: dist-sdist + path: dist + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: repro-rebuild-sdist + path: dist-rebuild + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: repro-digest-sdist + path: repro-digest + + wheels: + # Disabled until self-hosted macOS, Windows and ARM release legs are available. + # Keep the full release matrix contract; downstream publication remains skipped. + if: ${{ false }} + name: wheels (${{ matrix.target }}, py${{ matrix.python-version }}) + needs: [verify-release, sdist] + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 90 + env: + SOURCE_DATE_EPOCH: ${{ needs.verify-release.outputs.source_date_epoch }} + CARGO_RELEASE_SHA: ${{ needs.verify-release.outputs.release_commit }} + CARGO_RELEASE_LEG: ${{ matrix.target }}-py${{ matrix.python-version }} + CARGO_BUILD_IMAGE: ${{ matrix.container }} + CARGO_BUILD_PYTHON: ${{ matrix.interpreter }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + manylinux: "2014" + python-version: "3.12" + interpreter: python3.12 + container: "quay.io/pypa/manylinux2014_x86_64@sha256:2fa5f99f660f115f547d96b4edcadfc19f77a6de8807734bf0fc4924f81be01a" + - runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + manylinux: "2014" + python-version: "3.13" + interpreter: python3.13 + container: "quay.io/pypa/manylinux2014_x86_64@sha256:2fa5f99f660f115f547d96b4edcadfc19f77a6de8807734bf0fc4924f81be01a" + - runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + manylinux: "2014" + python-version: "3.14" + interpreter: python3.14 + container: "quay.io/pypa/manylinux2014_x86_64@sha256:2fa5f99f660f115f547d96b4edcadfc19f77a6de8807734bf0fc4924f81be01a" + - runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + manylinux: "2014" + python-version: "3.12" + interpreter: python3.12 + container: "quay.io/pypa/manylinux2014_aarch64@sha256:47940daeb8e0fdcc7deefad334bfda3d8d12b447ba1ebd5411f9f106318eafdb" + - runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + manylinux: "2014" + python-version: "3.13" + interpreter: python3.13 + container: "quay.io/pypa/manylinux2014_aarch64@sha256:47940daeb8e0fdcc7deefad334bfda3d8d12b447ba1ebd5411f9f106318eafdb" + - runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + manylinux: "2014" + python-version: "3.14" + interpreter: python3.14 + container: "quay.io/pypa/manylinux2014_aarch64@sha256:47940daeb8e0fdcc7deefad334bfda3d8d12b447ba1ebd5411f9f106318eafdb" + - runner: macos-latest + target: universal2-apple-darwin + manylinux: "" + python-version: "3.12" + interpreter: python + container: "" + - runner: macos-latest + target: universal2-apple-darwin + manylinux: "" + python-version: "3.13" + interpreter: python + container: "" + - runner: macos-latest + target: universal2-apple-darwin + manylinux: "" + python-version: "3.14" + interpreter: python + container: "" + - runner: windows-latest + target: x86_64-pc-windows-msvc + manylinux: "" + python-version: "3.12" + interpreter: python + container: "" + - runner: windows-latest + target: x86_64-pc-windows-msvc + manylinux: "" + python-version: "3.13" + interpreter: python + container: "" + - runner: windows-latest + target: x86_64-pc-windows-msvc + manylinux: "" + python-version: "3.14" + interpreter: python + container: "" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + - name: Validate release ancestry in this job + env: + CONTROL_PLANE_COMMIT: ${{ github.sha }} + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if ! printf '%s' "$RELEASE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "release_commit must be a canonical 40-character lowercase SHA-1" >&2 + exit 1 + fi + trusted_control_commit="$(git rev-parse HEAD)" + if [ "$trusted_control_commit" != "$CONTROL_PLANE_COMMIT" ]; then + echo "checked-out protected control plane does not match control_plane_commit" >&2 + exit 1 + fi + canonical_release_commit="$(git rev-parse --verify "$RELEASE_COMMIT^{commit}" 2>/dev/null)" || { + echo "release_commit is unavailable for ancestry verification" >&2 + exit 1 + } + if [ "$canonical_release_commit" != "$RELEASE_COMMIT" ]; then + echo "release_commit did not resolve to its canonical commit identity" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$canonical_release_commit" "$trusted_control_commit"; then + echo "release commit must be an ancestor of the publication control plane" >&2 + exit 1 + fi + git checkout --detach "$canonical_release_commit" + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + path: trusted-control + persist-credentials: false + # maturin-action forwards only allow-listed env prefixes (CARGO_, RUST, + # MATURIN_, ...) into the manylinux container, so SOURCE_DATE_EPOCH must + # be passed explicitly via docker-options for the Linux legs. Native + # macOS/Windows builds inherit it from the job env. + - name: Build wheel + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b + env: + CARGO_BUILD_PASS: first + with: + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux || 'auto' }} + container: ${{ matrix.container }} + maturin-version: v1.15.0 + docker-options: -e SOURCE_DATE_EPOCH + before-script-linux: $CARGO_BUILD_PYTHON trusted-control/scripts/ci/capture_release_build_scope.py + args: --release --out dist -i ${{ matrix.interpreter }} + - name: Capture first native-runner build graph + if: ${{ runner.os != 'Linux' }} + env: + CARGO_BUILD_PASS: first + run: python trusted-control/scripts/ci/capture_release_build_scope.py + # Clean-target second build (fresh cargo target dir) for EVERY leg. The + # reproducibility record fails closed on any publishable artifact + # without a matching byte-verified row. + - name: Rebuild wheel for byte-reproducibility check + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b + env: + CARGO_BUILD_PASS: second + with: + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux || 'auto' }} + container: ${{ matrix.container }} + maturin-version: v1.15.0 + docker-options: -e SOURCE_DATE_EPOCH + before-script-linux: $CARGO_BUILD_PYTHON trusted-control/scripts/ci/capture_release_build_scope.py + args: --release --out dist-rebuild --target-dir target-rebuild -i ${{ matrix.interpreter }} + - name: Capture second native-runner build graph + if: ${{ runner.os != 'Linux' }} + env: + CARGO_BUILD_PASS: second + run: python trusted-control/scripts/ci/capture_release_build_scope.py + - name: Compare double-build wheel digests and record them + shell: bash + env: + LEG: ${{ matrix.target }}-py${{ matrix.python-version }} + ARTIFACT_GLOB: "*.whl" + CONTAINER_IMAGE: ${{ matrix.container }} + run: | + set -euo pipefail + python - <<'PY' + import hashlib + import json + import os + import re + import subprocess + from pathlib import Path + + # Claim scope: a clean-target repeat build in the SAME runner and + # container environment. This is not independent-environment or + # independent-toolchain provenance. + leg = os.environ["LEG"] + pattern = os.environ["ARTIFACT_GLOB"] + + def digest(directory: str) -> tuple[str, str]: + found = sorted(Path(directory).glob(pattern)) + if len(found) != 1: + raise SystemExit(f"expected exactly one {pattern} in {directory}, found {found!r}") + return found[0].name, hashlib.sha256(found[0].read_bytes()).hexdigest() + + epoch = os.environ.get("SOURCE_DATE_EPOCH", "") + if not epoch.isdigit(): + raise SystemExit(f"SOURCE_DATE_EPOCH is not set to a release-commit epoch: {epoch!r}") + first = digest("dist") + second = digest("dist-rebuild") + if first != second: + raise SystemExit(f"{leg} is not byte-reproducible: {first!r} != {second!r}") + + # Provenance is bound to THIS invocation: Linux wheel legs build in + # the digest-pinned container passed to maturin-action, which must be + # the image present on the runner; every other row (macOS, Windows, + # sdist) ran without a container and records the runner identity. + # Unrelated cached images are never consulted. + container = os.environ.get("CONTAINER_IMAGE", "") + runner_os = os.environ.get("RUNNER_OS", "") + if runner_os == "Linux" and leg != "sdist": + if not re.fullmatch(r"[a-z0-9.\-]+(?:/[a-z0-9._\-]+)+@sha256:[0-9a-f]{64}", container): + raise SystemExit(f"{leg}: Linux wheel leg has no digest-pinned build container: {container!r}") + inspected = subprocess.run( + ["docker", "image", "inspect", "--format", "{{json .RepoDigests}}", container], + capture_output=True, text=True, + ) + if inspected.returncode != 0 or container not in json.loads(inspected.stdout or "[]"): + raise SystemExit(f"{leg}: pinned build container {container} is not present on this runner") + build_env = f"container:{container}" + else: + if container: + raise SystemExit(f"{leg}: ran without a container but carries container provenance {container!r}") + identity = [os.environ.get(key, "") for key in ("ImageOS", "ImageVersion", "RUNNER_OS", "RUNNER_ARCH")] + if not all(identity): + raise SystemExit(f"{leg}: missing runner identity (ImageOS/ImageVersion/RUNNER_OS/RUNNER_ARCH): {identity!r}") + build_env = "runner:" + "/".join(identity) + + name, sha = first + out = Path("repro-digest") + out.mkdir(exist_ok=True) + row = "\t".join([leg, "true", "clean-target-repeat-same-env", sha, second[1], name, build_env]) + (out / f"{leg}.tsv").write_text(row + "\n", encoding="utf-8") + print(row) + PY + - name: Capture finished wheel members from the build leg + shell: bash + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + LEG: ${{ matrix.target }}-py${{ matrix.python-version }} + run: python trusted-control/scripts/ci/capture_release_bundle.py "repro-digest/$LEG.tsv" dist + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 + with: + version: "0.12.5" + enable-cache: false + - name: Capture isolated locked runtime and all optional dependencies + shell: bash + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + LEG: ${{ matrix.target }}-py${{ matrix.python-version }} + run: | + set -euo pipefail + python trusted-control/scripts/ci/capture_release_runtime.py "repro-digest/$LEG.tsv" dist "$RUNNER_TEMP" + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: dist-sdist + path: sdist-input + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: repro-digest-sdist + path: sdist-proof + - name: Verify and unpack the same-run sdist + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + run: python trusted-control/scripts/ci/release_sdist_consumer.py prepare sdist-proof/sdist.tsv sdist-input sdist-consumer + - name: Build this wheel target from the verified sdist + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b + with: + command: build + working-directory: sdist-consumer/source + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux || 'auto' }} + container: ${{ matrix.container }} + maturin-version: v1.15.0 + docker-options: -e SOURCE_DATE_EPOCH + args: --release --out dist-consumer -i ${{ matrix.interpreter }} + - name: Capture target sdist consumer wheel + shell: bash + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + LEG: ${{ matrix.target }}-py${{ matrix.python-version }} + run: python trusted-control/scripts/ci/release_sdist_consumer.py capture "repro-digest/$LEG.tsv" dist sdist-consumer repro-digest + - name: Install target sdist consumer wheel + shell: bash + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + LEG: ${{ matrix.target }}-py${{ matrix.python-version }} + run: python trusted-control/scripts/ci/release_sdist_consumer.py install "repro-digest/$LEG.tsv" repro-digest "$RUNNER_TEMP" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: dist-wheel-${{ matrix.target }}-py${{ matrix.python-version }} + path: dist + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: repro-rebuild-${{ matrix.target }}-py${{ matrix.python-version }} + path: dist-rebuild + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: repro-digest-${{ matrix.target }}-py${{ matrix.python-version }} + path: repro-digest + + macos-x86-runtime: + if: ${{ false }} + name: macOS x86 runtime ${{ matrix.python-version }} + needs: [verify-release, wheels] + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 20 + permissions: + contents: read + actions: read + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14"] + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + LEG: universal2-apple-darwin-py${{ matrix.python-version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + - name: Validate release ancestry in this job + env: + CONTROL_PLANE_COMMIT: ${{ github.sha }} + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if ! printf '%s' "$RELEASE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "release_commit must be a canonical 40-character lowercase SHA-1" >&2 + exit 1 + fi + trusted_control_commit="$(git rev-parse HEAD)" + if [ "$trusted_control_commit" != "$CONTROL_PLANE_COMMIT" ]; then + echo "checked-out protected control plane does not match control_plane_commit" >&2 + exit 1 + fi + canonical_release_commit="$(git rev-parse --verify "$RELEASE_COMMIT^{commit}" 2>/dev/null)" || { + echo "release_commit is unavailable for ancestry verification" >&2 + exit 1 + } + if [ "$canonical_release_commit" != "$RELEASE_COMMIT" ]; then + echo "release_commit did not resolve to its canonical commit identity" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$canonical_release_commit" "$trusted_control_commit"; then + echo "release commit must be an ancestor of the publication control plane" >&2 + exit 1 + fi + git checkout --detach "$canonical_release_commit" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + path: trusted-control + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 + with: + version: "0.12.5" + enable-cache: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: dist-wheel-${{ env.LEG }} + path: dist + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: repro-digest-${{ env.LEG }} + path: build-proof + - name: Capture x86_64 installed dependencies and imported wheel + run: | + set -euo pipefail + test "$(uname -m)" = x86_64 + test "$(python -c 'import platform; print(platform.machine())')" = x86_64 + mkdir runtime-proof + cp "build-proof/$LEG.tsv" "runtime-proof/$LEG.tsv" + python trusted-control/scripts/ci/capture_release_runtime.py "runtime-proof/$LEG.tsv" dist "$RUNNER_TEMP" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: repro-macos-x86-${{ env.LEG }} + path: runtime-proof + + # Publication gate: release-assets and publish-pypi need this job, so a + # publishable artifact without a matching byte-verified row blocks both. + reproducibility-record: + name: reproducibility record + needs: [verify-release, sdist, wheels, macos-x86-runtime] + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 5 + permissions: + contents: read + actions: read + outputs: + distribution_set_artifact_id: ${{ steps.record-upload.outputs.artifact-id }} + distribution_set_artifact_digest: sha256:${{ steps.record-upload.outputs.artifact-digest }} + selected_wheel_filename: ${{ steps.bind-distributions.outputs.selected_wheel_filename }} + selected_sdist_filename: ${{ steps.bind-distributions.outputs.selected_sdist_filename }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + path: trusted-control + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + fetch-tags: true + path: release-source + persist-credentials: false + - name: Validate release ancestry in this job + env: + CONTROL_PLANE_COMMIT: ${{ github.sha }} + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + working-directory: release-source + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if ! printf '%s' "$RELEASE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "release_commit must be a canonical 40-character lowercase SHA-1" >&2 + exit 1 + fi + trusted_control_commit="$(git rev-parse HEAD)" + if [ "$trusted_control_commit" != "$CONTROL_PLANE_COMMIT" ]; then + echo "checked-out protected control plane does not match control_plane_commit" >&2 + exit 1 + fi + canonical_release_commit="$(git rev-parse --verify "$RELEASE_COMMIT^{commit}" 2>/dev/null)" || { + echo "release_commit is unavailable for ancestry verification" >&2 + exit 1 + } + if [ "$canonical_release_commit" != "$RELEASE_COMMIT" ]; then + echo "release_commit did not resolve to its canonical commit identity" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$canonical_release_commit" "$trusted_control_commit"; then + echo "release commit must be an ancestor of the publication control plane" >&2 + exit 1 + fi + git checkout --detach "$canonical_release_commit" + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: dist-* + path: dist + merge-multiple: true + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: repro-digest-* + path: repro-digest + merge-multiple: false + - name: Require a byte-verified row for every publishable artifact + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + RELEASE_TAG: ${{ inputs.release_tag }} + SOURCE_DATE_EPOCH: ${{ needs.verify-release.outputs.source_date_epoch }} + run: | + set -euo pipefail + python3 - <<'PY' + import hashlib + import os + from pathlib import Path + + header = ["target", "byte_verified", "verification", "sha256", "rebuild_sha256", "file", "build_env"] + rows = {} + for path in sorted(Path("repro-digest").glob("repro-digest-*/*.tsv")): + for line in path.read_text(encoding="utf-8").splitlines(): + fields = line.split("\t") + if len(fields) != len(header): + raise SystemExit(f"malformed digest row in {path.name}: {line!r}") + row = dict(zip(header, fields)) + if row["file"] in rows: + raise SystemExit(f"duplicate digest row for {row['file']}") + rows[row["file"]] = row + + published = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(Path("dist").iterdir())} + if len(published) != 13: + raise SystemExit(f"expected 13 publishable artifacts (12 wheels + sdist), found {sorted(published)!r}") + failures = [] + for name, sha in published.items(): + row = rows.get(name) + if row is None: + failures.append(f"{name}: no byte-verification row") + elif row["byte_verified"] != "true" or row["sha256"] != sha or row["rebuild_sha256"] != sha: + failures.append(f"{name}: not byte-verified against the published bytes ({row!r}, published {sha})") + for name in sorted(set(rows) - set(published)): + failures.append(f"{name}: digest row without a publishable artifact") + if failures: + raise SystemExit("reproducibility gate failed:\n" + "\n".join(failures)) + + record = Path("reproducibility-record.tsv") + # Existing matrix rows retain the build environment. This job only + # binds declarations; its Ubuntu host is not a target closure oracle. + import importlib.util + import json + spec = importlib.util.spec_from_file_location("transport", "trusted-control/scripts/ci/release_artifact_transport.py") + transport = importlib.util.module_from_spec(spec) + spec.loader.exec_module(transport) + scope_records = [transport.scope_identity( + Path("dist") / row["file"], row["target"], Path("release-source"), + os.environ["RELEASE_COMMIT"], row["build_env"], + ) for row in rows.values()] + Path("release-scope-identities.json").write_text(json.dumps(scope_records, sort_keys=True) + "\n") + with record.open("w", encoding="utf-8") as stream: + stream.write( + f"# release {os.environ['RELEASE_TAG']} @ {os.environ['RELEASE_COMMIT']}, " + f"SOURCE_DATE_EPOCH={os.environ['SOURCE_DATE_EPOCH']}\n" + ) + stream.write("\t".join(header) + "\n") + for name in sorted(rows): + stream.write("\t".join(rows[name][key] for key in header) + "\n") + summary = ( + "## Release artifact digests\n\n" + f"Byte-verified: {len(rows)} of {len(published)} publishable artifacts; single-built only: 0.\n\n" + "Claim scope: every row is a clean-target repeat build in the SAME runner/container " + "environment (build_env column). This is NOT independent-environment or " + "independent-toolchain provenance.\n\n" + "```\n" + record.read_text(encoding="utf-8") + "```\n" + ) + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as stream: + stream.write(summary) + print(record.read_text(encoding="utf-8")) + PY + - name: List distribution artifacts from this run + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh api --paginate "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100" \ + --jq '.artifacts[]' > run-artifacts.jsonl + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/attempts/$GITHUB_RUN_ATTEMPT" \ + > run-attempt.json + - name: Bind all verified distribution bytes to immutable artifact IDs + id: bind-distributions + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + RELEASE_TAG: ${{ inputs.release_tag }} + EXPECTED_WHEEL_LEGS: "x86_64-unknown-linux-gnu-py3.12 x86_64-unknown-linux-gnu-py3.13 x86_64-unknown-linux-gnu-py3.14 aarch64-unknown-linux-gnu-py3.12 aarch64-unknown-linux-gnu-py3.13 aarch64-unknown-linux-gnu-py3.14 universal2-apple-darwin-py3.12 universal2-apple-darwin-py3.13 universal2-apple-darwin-py3.14 x86_64-pc-windows-msvc-py3.12 x86_64-pc-windows-msvc-py3.13 x86_64-pc-windows-msvc-py3.14" + run: | + set -euo pipefail + python3 - <<'PY' + import hashlib + import json + import os + import re + from datetime import datetime + from pathlib import Path + + legs = os.environ["EXPECTED_WHEEL_LEGS"].split() + expected_legs = set(legs) | {"sdist"} + if len(legs) != 12 or len(expected_legs) != 13: + raise SystemExit("distribution set requires 12 distinct wheel legs and one sdist") + lines = Path("reproducibility-record.tsv").read_text(encoding="utf-8").splitlines() + if len(lines) != 15 or not lines[0].startswith( + f"# release {os.environ['RELEASE_TAG']} @ {os.environ['RELEASE_COMMIT']}, SOURCE_DATE_EPOCH=" + ) or lines[1].split("\t") != ["target", "byte_verified", "verification", "sha256", "rebuild_sha256", "file", "build_env"]: + raise SystemExit("reproducibility record header or row count differs from this release") + rows = {} + for line in lines[2:]: + fields = line.split("\t") + if len(fields) != 7 or fields[0] in rows: + raise SystemExit("duplicate or malformed distribution row") + rows[fields[0]] = dict(zip(lines[1].split("\t"), fields)) + if set(rows) != expected_legs: + raise SystemExit("reproducibility record does not cover the exact distribution legs") + files = {row["file"] for row in rows.values()} + paths = list(Path("dist").iterdir()) + if (len(files) != 13 or len(paths) != 13 or files != {p.name for p in paths} + or any(p.is_symlink() or not p.is_file() for p in paths)): + raise SystemExit("distribution files differ from the record") + + seen = {} + for line in Path("run-artifacts.jsonl").read_text(encoding="utf-8").splitlines(): + artifact = json.loads(line) + name = artifact.get("name") + if name in seen: + raise SystemExit(f"duplicate same-run artifact name: {name}") + seen[name] = artifact + expected_names = {"dist-sdist"} | {f"dist-wheel-{leg}" for leg in legs} + if {name for name in seen if isinstance(name, str) and name.startswith("dist-")} != expected_names: + raise SystemExit("distribution artifact names differ from the exact 13-leg set") + run_id = int(os.environ["GITHUB_RUN_ID"]) + attempt = int(os.environ["GITHUB_RUN_ATTEMPT"]) + control_sha = os.environ["GITHUB_SHA"] + if (run_id < 1 or attempt < 1 + or not re.fullmatch(r"[0-9a-f]{40}", os.environ["RELEASE_COMMIT"]) + or not re.fullmatch(r"[0-9a-f]{40}", control_sha)): + raise SystemExit("invalid release execution identity") + run = json.loads(Path("run-attempt.json").read_text(encoding="utf-8")) + if (run.get("id") != run_id or run.get("run_attempt") != attempt + or run.get("head_sha") != control_sha): + raise SystemExit("workflow attempt does not match the release control plane") + def timestamp(value): + if not isinstance(value, str): + raise SystemExit("missing workflow or artifact timestamp") + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + raise SystemExit("workflow or artifact timestamp has no timezone") + return parsed + attempt_started = timestamp(run.get("run_started_at")) + distributions = [] + ids = set() + for leg, row in sorted(rows.items()): + path = Path("dist") / row["file"] + with path.open("rb") as stream: + sha = hashlib.file_digest(stream, "sha256").hexdigest() + if (not re.fullmatch(r"[0-9a-f]{64}", row["sha256"]) + or row["byte_verified"] != "true" or row["rebuild_sha256"] != sha + or row["sha256"] != sha or path.name != row["file"]): + raise SystemExit(f"{leg}: distribution bytes are not reproducibility verified") + name = "dist-sdist" if leg == "sdist" else f"dist-wheel-{leg}" + artifact = seen[name] + artifact_id = artifact.get("id") + digest = artifact.get("digest") + if (type(artifact_id) is not int or artifact_id <= 0 or artifact_id in ids + or not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest) + or (artifact.get("workflow_run") or {}).get("id") != run_id + or (artifact.get("workflow_run") or {}).get("head_sha") != control_sha + or timestamp(artifact.get("created_at")) <= attempt_started + or artifact.get("expired") is not False): + raise SystemExit(f"{name}: missing or foreign immutable artifact identity") + ids.add(artifact_id) + distributions.append({"leg": leg, "file": row["file"], "sha256": sha, + "artifact_id": artifact_id, "artifact_name": name, + "artifact_digest": digest}) + scope_evidence = [] + expected_scope_names = {f"repro-digest-{leg}" for leg in expected_legs} + if {name for name in seen if name.startswith("repro-digest-")} != expected_scope_names: + raise SystemExit("scope evidence artifacts differ from the exact 13-leg set") + for leg in sorted(expected_legs): + name = f"repro-digest-{leg}" + artifact = seen[name] + artifact_id, digest = artifact.get("id"), artifact.get("digest") + if (type(artifact_id) is not int or artifact_id <= 0 or artifact_id in ids + or not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest) + or (artifact.get("workflow_run") or {}).get("id") != run_id + or (artifact.get("workflow_run") or {}).get("head_sha") != control_sha + or timestamp(artifact.get("created_at")) <= attempt_started + or artifact.get("expired") is not False): + raise SystemExit(f"{name}: missing or foreign immutable artifact identity") + ids.add(artifact_id) + scope_evidence.append({"leg": leg, "artifact_id": artifact_id, + "artifact_name": name, "artifact_digest": digest}) + manifest = {"schema_version": 1, "source_repository": os.environ["GITHUB_REPOSITORY"], + "source_sha": os.environ["RELEASE_COMMIT"], "control_sha": control_sha, "run_id": run_id, + "run_attempt": attempt, "distributions": distributions} + Path("release-gate-distribution-set.json").write_text( + json.dumps(manifest, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8" + ) + scope_set = {"schema_version": 1, "source_repository": os.environ["GITHUB_REPOSITORY"], + "source_sha": os.environ["RELEASE_COMMIT"], "control_sha": control_sha, + "run_id": run_id, "run_attempt": attempt, "evidence": scope_evidence} + Path("release-scope-evidence-set.json").write_text( + json.dumps(scope_set, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8" + ) + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"selected_wheel_filename={rows['x86_64-unknown-linux-gnu-py3.12']['file']}\n") + output.write(f"selected_sdist_filename={rows['sdist']['file']}\n") + PY + - id: record-upload + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: reproducibility-record + path: | + reproducibility-record.tsv + release-scope-identities.json + release-gate-distribution-set.json + release-scope-evidence-set.json + + dependency-gate: + name: complete dependency licence and Strix gate + needs: [verify-release, reproducibility-record] + uses: ContextualWisdomLab/.github/.github/workflows/release-dependency-license-strix-gate.yml@b6cebb36dc11afe409a7fee8a3262827255c029c + with: + source_repository: ${{ github.repository }} + source_sha: ${{ needs.verify-release.outputs.release_commit }} + ecosystems: python,cargo + python_lock_path: requirements/package.txt + cargo_manifest_path: crates/fast-mlsirm-py/Cargo.toml + cargo_dev_manifest_path: crates/mlsirm-core/Cargo.toml + distribution_set_artifact_id: ${{ needs.reproducibility-record.outputs.distribution_set_artifact_id }} + distribution_set_artifact_digest: ${{ needs.reproducibility-record.outputs.distribution_set_artifact_digest }} + wheel_filename: ${{ needs.reproducibility-record.outputs.selected_wheel_filename }} + sdist_filename: ${{ needs.reproducibility-record.outputs.selected_sdist_filename }} + secrets: inherit + + # R5b/R5c: nothing is tagged, released or published until the exact thirteen + # files match both the reproducibility record and a successful same-run + # central licence/Strix verdict and verified target-specific scope evidence. + # This job creates only a manifest; the following jobs create release state. + release-admission: + name: release admission + needs: [verify-release, reproducibility-record, dependency-gate] + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 10 + permissions: + contents: read + actions: read + env: + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + RELEASE_TAG: ${{ inputs.release_tag }} + REPOSITORY: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} + EXPECTED_WHEEL_LEGS: "x86_64-unknown-linux-gnu-py3.12 x86_64-unknown-linux-gnu-py3.13 x86_64-unknown-linux-gnu-py3.14 aarch64-unknown-linux-gnu-py3.12 aarch64-unknown-linux-gnu-py3.13 aarch64-unknown-linux-gnu-py3.14 universal2-apple-darwin-py3.12 universal2-apple-darwin-py3.13 universal2-apple-darwin-py3.14 x86_64-pc-windows-msvc-py3.12 x86_64-pc-windows-msvc-py3.13 x86_64-pc-windows-msvc-py3.14" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + fetch-tags: true + path: release-source + persist-credentials: false + - name: Validate release ancestry in this job + env: + CONTROL_PLANE_COMMIT: ${{ github.sha }} + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + working-directory: release-source + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if ! printf '%s' "$RELEASE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "release_commit must be a canonical 40-character lowercase SHA-1" >&2 + exit 1 + fi + trusted_control_commit="$(git rev-parse HEAD)" + if [ "$trusted_control_commit" != "$CONTROL_PLANE_COMMIT" ]; then + echo "checked-out protected control plane does not match control_plane_commit" >&2 + exit 1 + fi + canonical_release_commit="$(git rev-parse --verify "$RELEASE_COMMIT^{commit}" 2>/dev/null)" || { + echo "release_commit is unavailable for ancestry verification" >&2 + exit 1 + } + if [ "$canonical_release_commit" != "$RELEASE_COMMIT" ]; then + echo "release_commit did not resolve to its canonical commit identity" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$canonical_release_commit" "$trusted_control_commit"; then + echo "release commit must be an ancestor of the publication control plane" >&2 + exit 1 + fi + git checkout --detach "$canonical_release_commit" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + path: trusted-control + persist-credentials: false + - name: List this run's artifacts + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh api --paginate "repos/$REPOSITORY/actions/runs/$RUN_ID/artifacts?per_page=100" \ + --jq '.artifacts[]' > run-artifacts.jsonl + gh api "repos/$REPOSITORY/actions/runs/$RUN_ID/attempts/$GITHUB_RUN_ATTEMPT" \ + > run-attempt.json + - name: Require the exact same-run artifact set + env: + RECORD_ID: ${{ needs.reproducibility-record.outputs.distribution_set_artifact_id }} + RECORD_DIGEST: ${{ needs.reproducibility-record.outputs.distribution_set_artifact_digest }} + VERDICT_ID: ${{ needs.dependency-gate.outputs.full_set_verdict_artifact_id }} + VERDICT_DIGEST: ${{ needs.dependency-gate.outputs.full_set_verdict_artifact_digest }} + VERDICT_NAME: ${{ format('{0}--full-set-verdict', needs.dependency-gate.outputs.evidence_artifact_name) }} + SEALED_NAME: ${{ needs.dependency-gate.outputs.evidence_artifact_name }} + SEALED_ID: ${{ needs.dependency-gate.outputs.evidence_artifact_id }} + SEALED_DIGEST: ${{ needs.dependency-gate.outputs.evidence_artifact_digest }} + run: | + set -euo pipefail + python3 - <<'PY' + import json + import os + import re + + # The central job's success authenticates the complete verdict; + # immutable IDs and digests bind its two evidence artifacts here. + legs = os.environ["EXPECTED_WHEEL_LEGS"].split() + if len(legs) != 12 or len(set(legs)) != 12: + raise SystemExit(f"expected 12 distinct wheel legs, got {legs!r}") + run_id = int(os.environ["RUN_ID"]) + seen: dict[str, dict] = {} + with open("run-artifacts.jsonl", encoding="utf-8") as stream: + for line in stream: + if not line.strip(): + continue + artifact = json.loads(line) + name = artifact.get("name", "") + if name in seen: + raise SystemExit(f"duplicate artifact name in this run: {name}") + seen[name] = artifact + distributions = {"dist-sdist"} | {f"dist-wheel-{leg}" for leg in legs} + scope_evidence = {"repro-digest-sdist"} | {f"repro-digest-{leg}" for leg in legs} + intel_evidence = {f"repro-macos-x86-universal2-apple-darwin-py{version}" + for version in ("3.12", "3.13", "3.14")} + evidence = {os.environ["VERDICT_NAME"], os.environ["SEALED_NAME"]} + failures = [] + selected = [] + ids = set() + missing_evidence = sorted(evidence - set(seen)) + if missing_evidence: + failures.append(f"central release evidence missing: {missing_evidence}") + for name in sorted(distributions | scope_evidence | intel_evidence | evidence | {"reproducibility-record"}): + artifact = seen.get(name) + if artifact is None: + failures.append(f"{name}: not uploaded by this run") + continue + if (artifact.get("workflow_run") or {}).get("id") != run_id: + failures.append(f"{name}: not produced by run {run_id}") + if artifact.get("expired") is not False: + failures.append(f"{name}: expired or expiry unknown") + if not re.fullmatch(r"sha256:[0-9a-f]{64}", artifact.get("digest") or ""): + failures.append(f"{name}: no sha256 artifact digest") + artifact_id = artifact.get("id") + if type(artifact_id) is not int or artifact_id <= 0 or artifact_id in ids: + failures.append(f"{name}: missing or duplicate immutable artifact ID") + else: + ids.add(artifact_id) + if name == "reproducibility-record" and ( + str(artifact_id) != os.environ["RECORD_ID"] + or artifact.get("digest") != os.environ["RECORD_DIGEST"] + ): + failures.append("reproducibility record differs from producer output") + if name == os.environ["VERDICT_NAME"] and ( + str(artifact_id) != os.environ["VERDICT_ID"] + or artifact.get("digest") != os.environ["VERDICT_DIGEST"] + ): + failures.append("full-set verdict differs from central gate output") + if name == os.environ["SEALED_NAME"] and ( + str(artifact_id) != os.environ["SEALED_ID"] + or artifact.get("digest") != os.environ["SEALED_DIGEST"] + ): + failures.append("sealed pair evidence differs from central gate output") + selected.append({"id": artifact_id, "name": name, "digest": artifact.get("digest")}) + unexpected = sorted( + name for name in seen + if name.startswith(("dist-", "repro-digest-", "repro-macos-x86-", "license-evidence-")) + and name not in distributions | scope_evidence | intel_evidence | evidence + ) + failures += [f"{name}: unexpected publishable or evidence artifact" for name in unexpected] + if failures: + raise SystemExit("release admission refused:\n" + "\n".join(failures)) + with open("selected-artifacts.json", "w", encoding="utf-8") as output: + json.dump(selected, output, sort_keys=True) + print(f"same-run artifacts: {len(distributions)} distributions and {len(evidence)} central evidence bundles") + PY + - name: Download selected immutable IDs and verify archive digests + env: + GH_TOKEN: ${{ github.token }} + run: python3 -I trusted-control/scripts/ci/release_artifact_transport.py "$REPOSITORY" + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 + with: + version: "0.12.5" + enable-cache: false + - name: Recompute the complete source dependency export + run: | + set -euo pipefail + uv export --project release-source --locked --offline --no-dev --no-emit-project --all-extras \ + --format requirements.txt --no-header --output-file "$RUNNER_TEMP/release-requirements.txt" + python3 - <<'PY' + import os + from pathlib import Path + + expected = (Path(os.environ["RUNNER_TEMP"]) / "release-requirements.txt").read_bytes() + for leg in os.environ["EXPECTED_WHEEL_LEGS"].split(): + captured = (Path("downloaded") / f"repro-digest-{leg}" / + f"{leg}.runtime-requirements.txt").read_bytes() + lines = captured.split(b"\n", 2) + if (len(lines) != 3 + or lines[0] != b"# This file was autogenerated by uv via the following command:" + or not lines[1].startswith(b"# uv export ") + or lines[2] != expected): + raise SystemExit(f"{leg}: runtime requirements differ from release source export") + if leg.startswith("universal2-apple-darwin-"): + intel = (Path("downloaded") / f"repro-macos-x86-{leg}" / + f"{leg}.runtime-requirements.txt").read_bytes().split(b"\n", 2) + if (len(intel) != 3 or intel[0] != lines[0] + or not intel[1].startswith(b"# uv export ") + or intel[2] != expected): + raise SystemExit(f"{leg}: Intel runtime requirements differ from release source export") + PY + - name: Provision hash-locked metadata parser + run: | + set -euo pipefail + uv venv "$RUNNER_TEMP/release-admission-venv" + uv pip install --python "$RUNNER_TEMP/release-admission-venv/bin/python" \ + --require-hashes -r trusted-control/requirements/package.txt + - name: Admit exactly the verified bytes of release_commit + env: + RECORD_ID: ${{ needs.reproducibility-record.outputs.distribution_set_artifact_id }} + RECORD_DIGEST: ${{ needs.reproducibility-record.outputs.distribution_set_artifact_digest }} + VERDICT_ID: ${{ needs.dependency-gate.outputs.full_set_verdict_artifact_id }} + VERDICT_DIGEST: ${{ needs.dependency-gate.outputs.full_set_verdict_artifact_digest }} + VERDICT_NAME: ${{ format('{0}--full-set-verdict', needs.dependency-gate.outputs.evidence_artifact_name) }} + run: | + set -euo pipefail + "$RUNNER_TEMP/release-admission-venv/bin/python" - <<'PY' + import hashlib + import importlib.util + import io + import json + import os + import re + import zipfile + from pathlib import Path + + spec = importlib.util.spec_from_file_location("transport", "trusted-control/scripts/ci/release_artifact_transport.py") + transport = importlib.util.module_from_spec(spec) + spec.loader.exec_module(transport) + selected = json.loads(Path("selected-artifacts.json").read_text()) + receipt = json.loads(Path("transport-receipt.json").read_text()) + transport.verify_materialized(selected, receipt, Path("downloaded")) + + def sha256(path: Path) -> str: + return transport.hash_file(path) + + legs = os.environ["EXPECTED_WHEEL_LEGS"].split() + commit, tag = os.environ["RELEASE_COMMIT"], os.environ["RELEASE_TAG"] + failures = [] + + # Source and same-release binding of the reproducibility record. + lines = Path("downloaded/reproducibility-record/reproducibility-record.tsv").read_text(encoding="utf-8").splitlines() + if not lines or not lines[0].startswith(f"# release {tag} @ {commit}, SOURCE_DATE_EPOCH="): + raise SystemExit(f"reproducibility record is not bound to {tag} @ {commit}: {lines[:1]!r}") + header = lines[1].split("\t") + rows = {} + filenames = set() + for line in lines[2:]: + if len(line.split("\t")) != len(header): + raise SystemExit("malformed reproducibility row") + row = dict(zip(header, line.split("\t"))) + if row["target"] in rows or row["file"] in filenames: + raise SystemExit("duplicate record target or filename") + filenames.add(row["file"]) + rows[row["target"]] = row + if set(rows) != set(legs) | {"sdist"}: + raise SystemExit(f"record legs {sorted(rows)!r} are not the 12 wheel legs + sdist") + if len(filenames) != 13: + raise SystemExit("expected 13 unique distribution filenames") + for leg in legs: + target, python = leg.rsplit("-py", 1) + cp = "cp" + python.replace(".", "") + platforms = { + "x86_64-unknown-linux-gnu": r"manylinux(?:2014|_2_[0-9]+)_x86_64", + "aarch64-unknown-linux-gnu": r"manylinux(?:2014|_2_[0-9]+)_aarch64", + "universal2-apple-darwin": r"macosx_[0-9]+_[0-9]+_(?:x86_64|arm64|universal2)", + "x86_64-pc-windows-msvc": r"win_amd64", + } + parts = rows[leg]["file"].removesuffix(".whl").rsplit("-", 3) + if (not rows[leg]["file"].endswith(".whl") or len(parts) != 4 + or parts[1:3] != [cp, cp] + or not all(re.fullmatch(platforms[target], p) for p in parts[3].split(".")) + or (target == "universal2-apple-darwin" + and not any(p.endswith("_universal2") for p in parts[3].split(".")))): + raise SystemExit(f"{leg}: wheel filename platform/ABI mismatch") + if not rows["sdist"]["file"].endswith(".tar.gz"): + raise SystemExit("sdist filename must end with .tar.gz") + for leg, row in rows.items(): + if row["byte_verified"] != "true" or row["sha256"] != row["rebuild_sha256"]: + failures.append(f"{leg}: record row is not byte-verified") + + # The 13 distribution files are exactly the recorded bytes. + dist = {} + for leg, row in rows.items(): + artifact_name = "dist-sdist" if leg == "sdist" else f"dist-wheel-{leg}" + members = list((Path("downloaded") / artifact_name).iterdir()) + if len(members) != 1 or members[0].name != row["file"]: + raise SystemExit(f"{leg}: distribution files differ from the record") + dist[members[0].name] = sha256(members[0]) + if dist[members[0].name] != row["sha256"]: + raise SystemExit(f"{leg}: distribution files differ from the record") + if leg != "sdist": + try: + with zipfile.ZipFile(members[0]) as archive: + metadata = [p for p in archive.namelist() if p.endswith(".dist-info/WHEEL")] + if len(metadata) != 1: + raise ValueError("expected one WHEEL metadata member") + with archive.open(metadata[0]) as wheel_metadata: + tags = [line[5:].rstrip("\r\n") for line in io.TextIOWrapper(wheel_metadata, encoding="utf-8") if line.startswith("Tag: ")] + parts = row["file"][:-4].rsplit("-", 3) + expected_tags = {f"{parts[1]}-{parts[2]}-{p}" for p in parts[3].split(".")} + if len(tags) != len(set(tags)) or set(tags) != expected_tags: + raise ValueError("WHEEL tags differ from filename ABI/platform") + except (OSError, ValueError, zipfile.BadZipFile, UnicodeError) as error: + raise SystemExit(f"{leg}: wheel metadata invalid ({error})") + recorded = {row["file"]: row["sha256"] for row in rows.values()} + if dist != recorded: + failures.append(f"distribution files differ from the record: files={sorted(dist)!r}") + if failures: + raise SystemExit("release admission refused:\n" + "\n".join(failures)) + + scope_set = json.loads(Path("downloaded/reproducibility-record/release-scope-evidence-set.json").read_text()) + scope_names = {f"repro-digest-{leg}" for leg in rows} + expected_scope_set = { + "schema_version": 1, "source_repository": os.environ["REPOSITORY"], + "source_sha": commit, "control_sha": os.environ["GITHUB_SHA"], + "run_id": int(os.environ["RUN_ID"]), "run_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]), + "evidence": [ + {"leg": leg, "artifact_id": item["id"], "artifact_name": name, + "artifact_digest": item["digest"]} + for leg in sorted(rows) + for name in [f"repro-digest-{leg}"] + for item in selected if item["name"] == name + ], + } + if (scope_set != expected_scope_set or len(scope_set.get("evidence", [])) != 13 + or {item["name"] for item in selected if item["name"].startswith("repro-digest-")} != scope_names): + raise SystemExit("scope evidence set differs from selected same-run artifacts") + def unique_object(pairs): + value = {} + for key, item in pairs: + if key in value: + raise ValueError(f"duplicate bundle inventory key: {key}") + value[key] = item + return value + runtime_records = [] + build_records = [] + build_graph_receipts = [] + for leg, row in rows.items(): + name = f"repro-digest-{leg}" + members = {path.name: path for path in (Path("downloaded") / name).iterdir()} + expected_members = {f"{leg}.tsv", f"{leg}.bundle.json", + f"{leg}.build-first.json", f"{leg}.build-second.json", + f"{leg}.build-python.zip"} + if leg != "sdist": + expected_members |= {f"{leg}.runtime.json", f"{leg}.runtime-requirements.txt", + f"{leg}.consumer.json", f"{leg}.consumer.whl"} + if (leg == "sdist" and set(members) != expected_members + or leg != "sdist" and not expected_members <= set(members)): + raise SystemExit(f"{leg}: scope evidence artifact members differ from build output") + if members[f"{leg}.tsv"].read_text(encoding="utf-8") != "\t".join(row[key] for key in header) + "\n": + raise SystemExit(f"{leg}: scope evidence row differs from reproducibility record") + artifact_name = "dist-sdist" if leg == "sdist" else f"dist-wheel-{leg}" + distribution = Path("downloaded") / artifact_name / row["file"] + expected_bundle = transport.bundle_inventory(distribution, leg, commit, row["build_env"]) + observed_bundle = json.loads(members[f"{leg}.bundle.json"].read_text(encoding="utf-8"), + object_pairs_hook=unique_object) + if observed_bundle != expected_bundle: + raise SystemExit(f"{leg}: build-leg bundle inventory differs from distribution bytes") + build_first = json.loads(members[f"{leg}.build-first.json"].read_text(encoding="utf-8"), + object_pairs_hook=unique_object) + build_second = json.loads(members[f"{leg}.build-second.json"].read_text(encoding="utf-8"), + object_pairs_hook=unique_object) + transport.verify_build_scope(build_first, build_second, row, Path("release-source"), + commit, members[f"{leg}.build-python.zip"]) + build_graph_receipts.append(build_first) + build_records.append({key: build_first[key] for key in + ("leg", "python_packages", "python_snapshot_sha256", + "build_env", "maturin_version", "maturin_binary_sha256")}) + if leg != "sdist": + runtime = json.loads(members[f"{leg}.runtime.json"].read_text(encoding="utf-8"), + object_pairs_hook=unique_object) + transport.verify_runtime_inventory( + runtime, members[f"{leg}.runtime-requirements.txt"], row, + Path("release-source"), commit, expected_bundle, distribution, members, + ) + consumer = json.loads(members[f"{leg}.consumer.json"].read_text(encoding="utf-8"), + object_pairs_hook=unique_object) + transport.verify_sdist_consumer( + consumer, members[f"{leg}.consumer.whl"], distribution, row, + rows["sdist"], commit, runtime, + ) + runtime_records.append(runtime) + + runtime_variants = [] + for version in ("3.12", "3.13", "3.14"): + leg = f"universal2-apple-darwin-py{version}" + row = rows[leg] + members = {path.name: path for path in + (Path("downloaded") / f"repro-macos-x86-{leg}").iterdir()} + if not {f"{leg}.tsv", f"{leg}.runtime.json", + f"{leg}.runtime-requirements.txt"} <= members.keys(): + raise SystemExit(f"{leg}: Intel runtime evidence is incomplete") + if members[f"{leg}.tsv"].read_text(encoding="utf-8") != "\t".join( + row[key] for key in header) + "\n": + raise SystemExit(f"{leg}: Intel runtime row differs from selected wheel") + distribution = Path("downloaded") / f"dist-wheel-{leg}" / row["file"] + bundle = transport.bundle_inventory(distribution, leg, commit, row["build_env"]) + runtime = json.loads(members[f"{leg}.runtime.json"].read_text(encoding="utf-8"), + object_pairs_hook=unique_object) + transport.verify_runtime_inventory( + runtime, members[f"{leg}.runtime-requirements.txt"], row, + Path("release-source"), commit, bundle, distribution, members, + runtime_only=True, + ) + runtime_variants.append(runtime) + + # The central job result is required by `needs`; its immutable verdict + # must independently match the record and all current-attempt artifacts. + verifier_spec = importlib.util.spec_from_file_location( + "full_verdict", "trusted-control/scripts/ci/verify_release_full_set_verdict.py" + ) + verifier = importlib.util.module_from_spec(verifier_spec) + verifier_spec.loader.exec_module(verifier) + verdict_name = os.environ["VERDICT_NAME"] + verdict_members = {path.name: path for path in (Path("downloaded") / verdict_name).iterdir()} + if set(verdict_members) != {"full-set-verdict.json", "gate-report.json", + "runtime-archive-license-report.json", "release-native-links.json"}: + raise SystemExit("central verdict artifact does not contain the exact dependency report") + verdict = json.loads(verdict_members["full-set-verdict.json"].read_text(encoding="utf-8"), + object_pairs_hook=unique_object) + report_bytes = verdict_members["gate-report.json"].read_bytes() + report = json.loads(report_bytes, object_pairs_hook=unique_object) + archive_report_bytes = verdict_members["runtime-archive-license-report.json"].read_bytes() + archive_report = json.loads(archive_report_bytes, object_pairs_hook=unique_object) + native_bytes = verdict_members["release-native-links.json"].read_bytes() + native = json.loads(native_bytes, object_pairs_hook=unique_object) + verifier.verify_full_set_verdict( + json.loads(Path("downloaded/reproducibility-record/release-gate-distribution-set.json").read_text()), + scope_set, + verdict, + [json.loads(line) for line in Path("run-artifacts.jsonl").read_text().splitlines()], + json.loads(Path("run-attempt.json").read_text()), + repository=os.environ["REPOSITORY"], source_sha=commit, + control_sha=os.environ["GITHUB_SHA"], run_id=int(os.environ["RUN_ID"]), + run_attempt=int(os.environ["GITHUB_RUN_ATTEMPT"]), + record_id=int(os.environ["RECORD_ID"]), record_digest=os.environ["RECORD_DIGEST"], + verdict_id=int(os.environ["VERDICT_ID"]), verdict_digest=os.environ["VERDICT_DIGEST"], + verdict_name=verdict_name, + ) + verifier.verify_runtime_dependency_coverage( + verdict, report, report_bytes, archive_report, archive_report_bytes, + runtime_records, runtime_variants, build_records, + repository=os.environ["REPOSITORY"], source_sha=commit, + ) + verifier.verify_native_link_inventory(verdict, native, native_bytes, + runtime_records, commit) + + # The same immutable-ID transport receipt covers this JSON alongside + # the TSV. Recompute its source/archive bindings before any admission. + scope_records = json.loads(Path("downloaded/reproducibility-record/release-scope-identities.json").read_text()) + distribution_paths = { + row["file"]: Path("downloaded") / ("dist-sdist" if leg == "sdist" else f"dist-wheel-{leg}") / row["file"] + for leg, row in rows.items() + } + transport.verify_scope_declarations(scope_records, rows, distribution_paths, Path("release-source"), commit) + for leg, row in rows.items(): + transport.verify_distribution_requirements(distribution_paths[row["file"]], leg, + Path("release-source"), commit) + transport.verify_bundled_license_notices( + transport.bundle_inventory(distribution_paths[row["file"]], leg, commit, row["build_env"]), + Path("release-source"), commit) + transport.verify_license_selection_report(report, Path("release-source"), commit) + transport.verify_cargo_graph_closure(build_graph_receipts, Path("release-source"), commit) + verifier.verify_cargo_dependency_coverage(report, build_graph_receipts, + source=Path("release-source"), source_sha=commit) + + # Source declarations cannot approve a closure. All collected scope + # receipts and the sealed licence/Strix set were verified above. + # Exclusive creation prevents a pre-existing file from being reused. + with Path("admitted-manifest.tsv").open("x", encoding="utf-8") as admitted: + for filename, digest in sorted(recorded.items()): + admitted.write(f"{digest} {filename}\n") + + PY + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: release-admission-manifest + path: admitted-manifest.tsv + + # R5a: the immutable tag and the GitHub release are created only here, after + # release admission. release-tag.yml verifies and dispatches but no longer + # creates either. A retry re-runs the whole build/record/admission chain; an + # existing tag or release is only resumed when it already targets + # release_commit, so no existing-tag path skips admission. + create-tag-and-release: + name: create tag and release after admission + needs: [verify-release, release-admission] + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + - name: Validate release ancestry in this job + env: + CONTROL_PLANE_COMMIT: ${{ github.sha }} + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + if ! printf '%s' "$RELEASE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "release_commit must be a canonical 40-character lowercase SHA-1" >&2 + exit 1 + fi + trusted_control_commit="$(git rev-parse HEAD)" + if [ "$trusted_control_commit" != "$CONTROL_PLANE_COMMIT" ]; then + echo "checked-out protected control plane does not match control_plane_commit" >&2 + exit 1 + fi + canonical_release_commit="$(git rev-parse --verify "$RELEASE_COMMIT^{commit}" 2>/dev/null)" || { + echo "release_commit is unavailable for ancestry verification" >&2 + exit 1 + } + if [ "$canonical_release_commit" != "$RELEASE_COMMIT" ]; then + echo "release_commit did not resolve to its canonical commit identity" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$canonical_release_commit" "$trusted_control_commit"; then + echo "release commit must be an ancestor of the publication control plane" >&2 + exit 1 + fi + git checkout --detach "$canonical_release_commit" + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: "3.12" + - name: Derive the release version from the admitted tag + env: + RELEASE_TAG: ${{ inputs.release_tag }} + run: | + set -euo pipefail + if ! printf '%s' "$RELEASE_TAG" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then + echo "release_tag must be v.., got $RELEASE_TAG" >&2 + exit 1 + fi + echo "RELEASE_VERSION=${RELEASE_TAG#v}" >> "$GITHUB_ENV" + - name: Fail closed on fragment to CHANGELOG aggregate drift + run: | + set -euo pipefail + python scripts/render_changelog_fragments.py --check CHANGELOG.md + - name: Extract the release notes from the CHANGELOG section + run: | + set -euo pipefail + awk -v header="## [$RELEASE_VERSION] - " ' + index($0, header) == 1 { capture = 1; next } + capture && /^## / { exit } + capture { print } + ' CHANGELOG.md > release_notes.md + if ! grep -q '[^[:space:]]' release_notes.md; then + echo "CHANGELOG section for $RELEASE_VERSION is empty" >&2 + exit 1 + fi + - name: Cap the release body below the GitHub release-body limit + run: | + set -euo pipefail + python3 - <<'PY' + import os + from pathlib import Path + + notes_path = Path("release_notes.md") + notes_text = notes_path.read_text(encoding="utf-8") + body_limit = 120_000 + if len(notes_text) > body_limit: + release_version = os.environ["RELEASE_VERSION"] + repository = os.environ["GITHUB_REPOSITORY"] + changelog_url = ( + f"https://github.com/{repository}/blob/" + f"v{release_version}/CHANGELOG.md" + ) + headings = list( + dict.fromkeys( + line + for line in notes_text.splitlines() + if line.startswith("#### ") + ) + ) + summary_lines = [ + "The complete notes for this release exceed GitHub's " + "release-body limit, so this body is a summary.", + "", + f"Full authoritative notes: the `[{release_version}]` " + f"section of [CHANGELOG.md]({changelog_url}).", + "", + "### Contents", + "", + ] + summary_lines += [ + "- " + line[len("#### "):] for line in headings + ] + capped_text = "\n".join(summary_lines) + "\n" + if len(capped_text) > body_limit: + capped_text = ( + capped_text[: body_limit - 60].rsplit("\n", 1)[0] + + "\n\n(contents list truncated)\n" + ) + notes_path.write_text(capped_text, encoding="utf-8") + PY + - name: Classify the tag and release state after admission + id: release_tag_state + env: + GH_TOKEN: ${{ github.token }} + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + run: | + set -euo pipefail + api_status() { + endpoint="$1" + response_file="$2" + curl --silent --show-error \ + --output "$response_file" \ + --write-out '%{http_code}' \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: Bearer $GH_TOKEN" \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/$endpoint" + } + response_file="$(mktemp)" + status="$(api_status "git/ref/tags/v$RELEASE_VERSION" "$response_file")" + case "$status" in + 404) + tag_exists=false + echo "resume_existing_tag=false" >> "$GITHUB_OUTPUT" + ;; + 200) + python3 - "$response_file" <<'PY' + import json + import os + import sys + from pathlib import Path + + response = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + tag_object = response.get("object", {}) + actual_tag_sha = tag_object.get("sha") + if ( + tag_object.get("type") != "commit" + or actual_tag_sha != os.environ["RELEASE_COMMIT"] + ): + raise SystemExit( + "existing tag does not target the requested release commit" + ) + PY + tag_exists=true + echo "tag v$RELEASE_VERSION already targets release_commit; resuming publication for the existing immutable tag" + echo "resume_existing_tag=true" >> "$GITHUB_OUTPUT" + ;; + *) + cat "$response_file" >&2 + echo "GitHub API returned HTTP $status while checking tag v$RELEASE_VERSION" >&2 + exit 1 + ;; + esac + status="$(api_status "releases/tags/v$RELEASE_VERSION" "$response_file")" + case "$status" in + 404) + echo "resume_existing_release=false" >> "$GITHUB_OUTPUT" + ;; + 200) + if [ "$tag_exists" != "true" ]; then + echo "release v$RELEASE_VERSION exists without its verified tag; refusing to reuse it" >&2 + exit 1 + fi + echo "release v$RELEASE_VERSION already exists for the verified tag; resuming without recreating it" + echo "resume_existing_release=true" >> "$GITHUB_OUTPUT" + ;; + *) + cat "$response_file" >&2 + echo "GitHub API returned HTTP $status while checking release v$RELEASE_VERSION" >&2 + exit 1 + ;; + esac + rm -f "$response_file" + - name: Atomically create the immutable release tag + if: steps.release_tag_state.outputs.resume_existing_tag != 'true' + env: + GH_TOKEN: ${{ github.token }} + RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} + run: | + set -euo pipefail + request_file="$(mktemp)" + response_file="$(mktemp)" + cleanup() { + rm -f "$request_file" "$response_file" + } + trap cleanup EXIT + + python3 - "$request_file" <<'PY' + import json + import os + import sys + from pathlib import Path + + Path(sys.argv[1]).write_text( + json.dumps( + { + "ref": f"refs/tags/v{os.environ['RELEASE_VERSION']}", + "sha": os.environ["RELEASE_COMMIT"], + } + ), + encoding="utf-8", + ) + PY + + status="$(curl --silent --show-error \ + --request POST \ + --output "$response_file" \ + --write-out '%{http_code}' \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: Bearer $GH_TOKEN" \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + --header "Content-Type: application/json" \ + --data-binary "@$request_file" \ + "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/refs")" + if [ "$status" != "201" ]; then + cat "$response_file" >&2 + echo "GitHub API returned HTTP $status while creating the release tag" >&2 + exit 1 + fi + + python3 - "$response_file" <<'PY' + import json + import os + import sys + from pathlib import Path + + response = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + expected_ref = f"refs/tags/v{os.environ['RELEASE_VERSION']}" + actual_ref = response.get("ref") + actual_sha = response.get("object", {}).get("sha") + if actual_ref != expected_ref or actual_sha != os.environ["RELEASE_COMMIT"]: + raise SystemExit( + "GitHub returned an unexpected ref or commit for the release tag" + ) + PY + - name: Publish the GitHub release from the verified tag + if: steps.release_tag_state.outputs.resume_existing_release != 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh release create "v$RELEASE_VERSION" \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --title "v$RELEASE_VERSION" \ + --notes-file release_notes.md + + release-assets: + name: attach release assets + needs: [sdist, wheels, reproducibility-record, release-admission, create-tag-and-release] + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: dist-* + path: dist + merge-multiple: true + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: release-admission-manifest + path: admission + - name: Require the admitted bytes + run: | + set -euo pipefail + (cd dist && sha256sum -- *) | sort -k2 > downloaded.tsv + sort -k2 admission/admitted-manifest.tsv > admitted.tsv + if ! diff -u admitted.tsv downloaded.tsv; then + echo "downloaded distributions are not the admitted bytes" >&2 + exit 1 + fi + - name: List built artifacts + run: ls -la dist + - name: Attach release assets + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.release_tag }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + # Immutable GitHub Releases reject later asset uploads (HTTP 422). + # Skip cleanly so a PyPI-only republish of an already-cut release is + # not forced into an overall workflow FAILURE by an unrecoverable + # asset sink, while still failing closed for mutable releases. + immutable="$(gh api "repos/$REPOSITORY/releases/tags/$RELEASE_TAG" --jq '.immutable // false')" + if [ "$immutable" = "true" ]; then + echo "release $RELEASE_TAG is immutable; skipping GitHub asset upload" + exit 0 + fi + # Per-file upload: already-present assets also return HTTP 422 + # (name already_exists / immutable). Treat those as success so + # retries after a partial attach remain green; other failures fail closed. + shopt -s nullglob + assets=(dist/*) + if [ "${#assets[@]}" -eq 0 ]; then + echo "no dist artifacts to attach" >&2 + exit 1 + fi + for asset in "${assets[@]}"; do + upload_log="$(mktemp)" + set +e + gh release upload "$RELEASE_TAG" "$asset" --repo "$REPOSITORY" >"$upload_log" 2>&1 + status=$? + set -e + if [ "$status" -eq 0 ]; then + rm -f "$upload_log" + continue + fi + if grep -Eiq 'HTTP 422|already[_ ]exists|immutable|unprocessable' "$upload_log"; then + echo "skipping already-uploaded or immutable asset: $asset" + cat "$upload_log" + rm -f "$upload_log" + continue + fi + cat "$upload_log" >&2 + rm -f "$upload_log" + echo "failed to attach release asset: $asset" >&2 + exit 1 + done + + publish-pypi: + name: publish to PyPI + needs: [sdist, wheels, reproducibility-record, release-admission, create-tag-and-release] + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 15 + environment: pypi + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: dist-* + path: dist + merge-multiple: true + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: release-admission-manifest + path: admission + - name: Require the admitted bytes + run: | + set -euo pipefail + (cd dist && sha256sum -- *) | sort -k2 > downloaded.tsv + sort -k2 admission/admitted-manifest.tsv > admitted.tsv + if ! diff -u admitted.tsv downloaded.tsv; then + echo "downloaded distributions are not the admitted bytes" >&2 + exit 1 + fi + - name: List built artifacts + run: ls -la dist + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 + with: + password: ${{ secrets.PIPY_TOKEN }} + attestations: false + # Partial publishes (wheels accepted, sdist rejected) must be + # retryable without failing on already-uploaded filenames. + skip-existing: true + - name: Verify published PyPI matrix against admission + run: python3 scripts/ci/verify_published_pypi.py admission/admitted-manifest.tsv '${{ inputs.release_tag }}' diff --git a/.github/workflows/fast-mlsirm-release-tag.yml b/.github/workflows/fast-mlsirm-release-tag.yml new file mode 100644 index 0000000000..fa804750ab --- /dev/null +++ b/.github/workflows/fast-mlsirm-release-tag.yml @@ -0,0 +1,343 @@ +name: Release Tag +on: + workflow_call: + inputs: + release_version: + description: "Release version to publish (e.g. 0.2.0); must match pyproject.toml" + required: true + type: string + release_commit: + description: "Release source commit reviewed for this version (full lowercase SHA-1)" + required: true + type: string + +permissions: + contents: read + +concurrency: + group: central-fast-mlsirm-release-tag-release-tag + cancel-in-progress: false + +jobs: + publish-release-tag: + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 10 + permissions: + contents: read + actions: write + steps: + - name: Verify dispatch targets the default branch + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + DISPATCH_REF: ${{ github.ref }} + run: | + set -euo pipefail + expected_ref="refs/heads/$DEFAULT_BRANCH" + if [ "$DISPATCH_REF" != "$expected_ref" ]; then + echo "release dispatch must target $expected_ref, not $DISPATCH_REF" >&2 + exit 1 + fi + - name: Validate release source identity + env: + RELEASE_COMMIT: ${{ inputs.release_commit }} + run: | + set -euo pipefail + if ! printf '%s' "$RELEASE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "release_commit must be a canonical 40-character lowercase SHA-1" >&2 + exit 1 + fi + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + fetch-depth: 0 + ref: ${{ github.sha }} + - name: Verify the release source is on the current default-branch lineage + env: + DISPATCH_SHA: ${{ github.sha }} + RELEASE_COMMIT: ${{ inputs.release_commit }} + run: | + set -euo pipefail + checked_out_commit="$(git rev-parse HEAD)" + if [ "$checked_out_commit" != "$DISPATCH_SHA" ]; then + echo "checked-out control plane does not match the default-branch dispatch" >&2 + exit 1 + fi + canonical_release_commit="$(git rev-parse --verify "$RELEASE_COMMIT^{commit}")" + if [ "$canonical_release_commit" != "$RELEASE_COMMIT" ]; then + echo "release_commit must identify a commit directly" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$RELEASE_COMMIT" "$DISPATCH_SHA"; then + echo "release commit must be an ancestor of the default branch" >&2 + exit 1 + fi + git checkout --detach "$canonical_release_commit" + if [ "$(git rev-parse HEAD)" != "$canonical_release_commit" ]; then + echo "checked-out release source does not match release_commit" >&2 + exit 1 + fi + - name: Verify the requested version is the released source version + env: + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + python3 - <<'PY' + import os + import re + import tomllib + from pathlib import Path + + release_version = os.environ["RELEASE_VERSION"] + core_semver = re.compile( + r"(?:0|[1-9][0-9]*)\." + r"(?:0|[1-9][0-9]*)\." + r"(?:0|[1-9][0-9]*)\Z" + ) + if core_semver.fullmatch(release_version) is None: + raise SystemExit( + "release_version must be a canonical three-component semantic " + "version without leading zeros" + ) + + project = tomllib.loads( + Path("pyproject.toml").read_text(encoding="utf-8") + ) + project_version = project["project"]["version"] + if project_version != release_version: + raise SystemExit( + f"pyproject.toml version {project_version} does not match " + f"requested {release_version}" + ) + + header = f"## [{release_version}] - " + matches = [ + line + for line in Path("CHANGELOG.md").read_text( + encoding="utf-8" + ).splitlines() + if line.startswith(header) + ] + if len(matches) != 1: + raise SystemExit( + f"expected exactly one CHANGELOG section for {release_version}; " + f"found {len(matches)}" + ) + PY + - name: Verify the release source is the version-cut transition + env: + RELEASE_COMMIT: ${{ inputs.release_commit }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + if ! parent_commit="$(git rev-parse "$RELEASE_COMMIT^" 2>/dev/null)"; then + echo "release commit must have a verifiable first parent" >&2 + exit 1 + fi + python3 - "$parent_commit" <<'PY' + import os + import subprocess + import sys + import tomllib + + parent_commit = sys.argv[1] + release_version = os.environ["RELEASE_VERSION"] + + def parent_text(path: str) -> str: + result = subprocess.run( + ["git", "show", f"{parent_commit}:{path}"], + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise SystemExit( + f"release parent is missing required source file {path}" + ) + return result.stdout + + parent_project = tomllib.loads(parent_text("pyproject.toml")) + parent_version = parent_project["project"]["version"] + if parent_version == release_version: + raise SystemExit( + "parent project version already equals requested release version" + ) + + header = f"## [{release_version}] - " + parent_changelog = parent_text("CHANGELOG.md").splitlines() + if any(line.startswith(header) for line in parent_changelog): + raise SystemExit( + "parent CHANGELOG already contains requested release section" + ) + PY + - name: Fail closed on fragment to CHANGELOG aggregate drift + run: | + set -euo pipefail + python scripts/render_changelog_fragments.py --check CHANGELOG.md + - name: Extract the release notes from the CHANGELOG section + env: + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + awk -v header="## [$RELEASE_VERSION] - " ' + index($0, header) == 1 { capture = 1; next } + capture && /^## / { exit } + capture { print } + ' CHANGELOG.md > release_notes.md + if ! grep -q '[^[:space:]]' release_notes.md; then + echo "CHANGELOG section for $RELEASE_VERSION is empty" >&2 + exit 1 + fi + - name: Keep internal monetary targets out of the public release notes + # The CHANGELOG section becomes the GitHub release body, which is a + # public distribution surface like the PyPI description. Naming what an + # entry removed is legitimate; restating the internal figure itself is + # the leak. v0.11.4 shipped with the amount spelled out before this ran. + run: | + set -euo pipefail + if grep -nEi 'KRW[[:space:]]*[0-9][0-9,]{6,}|[0-9]+[[:space:]]*(billion|B)[[:space:]]+(product|deal|contract)' release_notes.md; then + echo "release notes name an internal monetary target; describe the gate without the figure" >&2 + exit 1 + fi + + - name: Cap the release body below the GitHub release-body limit + env: + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + python3 - <<'PY' + import os + from pathlib import Path + + notes_path = Path("release_notes.md") + notes_text = notes_path.read_text(encoding="utf-8") + body_limit = 120_000 + if len(notes_text) > body_limit: + release_version = os.environ["RELEASE_VERSION"] + repository = os.environ["GITHUB_REPOSITORY"] + changelog_url = ( + f"https://github.com/{repository}/blob/" + f"v{release_version}/CHANGELOG.md" + ) + headings = list( + dict.fromkeys( + line + for line in notes_text.splitlines() + if line.startswith("#### ") + ) + ) + summary_lines = [ + "The complete notes for this release exceed GitHub's " + "release-body limit, so this body is a summary.", + "", + f"Full authoritative notes: the `[{release_version}]` " + f"section of [CHANGELOG.md]({changelog_url}).", + "", + "### Contents", + "", + ] + summary_lines += [ + "- " + line[len("#### "):] for line in headings + ] + capped_text = "\n".join(summary_lines) + "\n" + if len(capped_text) > body_limit: + capped_text = ( + capped_text[: body_limit - 60].rsplit("\n", 1)[0] + + "\n\n(contents list truncated)\n" + ) + notes_path.write_text(capped_text, encoding="utf-8") + PY + - name: Verify the release is absent and classify the tag state + id: release_tag_state + env: + GH_TOKEN: ${{ github.token }} + RELEASE_COMMIT: ${{ inputs.release_commit }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + api_status() { + endpoint="$1" + response_file="$2" + curl --silent --show-error \ + --output "$response_file" \ + --write-out '%{http_code}' \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: Bearer $GH_TOKEN" \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/$endpoint" + } + response_file="$(mktemp)" + status="$(api_status "releases/tags/v$RELEASE_VERSION" "$response_file")" + case "$status" in + 404) + ;; + 200) + echo "release v$RELEASE_VERSION already exists; refusing to overwrite or reuse it" >&2 + exit 1 + ;; + *) + cat "$response_file" >&2 + echo "GitHub API returned HTTP $status while checking release v$RELEASE_VERSION" >&2 + exit 1 + ;; + esac + status="$(api_status "git/ref/tags/v$RELEASE_VERSION" "$response_file")" + case "$status" in + 404) + echo "resume_existing_tag=false" >> "$GITHUB_OUTPUT" + ;; + 200) + python3 - "$response_file" <<'PY' + import json + import os + import sys + from pathlib import Path + + response = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + tag_object = response.get("object", {}) + actual_tag_sha = tag_object.get("sha") + if ( + tag_object.get("type") != "commit" + or actual_tag_sha != os.environ["RELEASE_COMMIT"] + ): + raise SystemExit( + "existing tag does not target the requested release commit" + ) + PY + echo "tag v$RELEASE_VERSION exists without a release; resuming publication for the existing immutable tag" + echo "resume_existing_tag=true" >> "$GITHUB_OUTPUT" + ;; + *) + cat "$response_file" >&2 + echo "GitHub API returned HTTP $status while checking tag v$RELEASE_VERSION" >&2 + exit 1 + ;; + esac + rm -f "$response_file" + # R5a: the tag and release are created by publish-pypi.yml only after + # release admission; this workflow verifies and dispatches. + - name: Dispatch package publication from the verified release source + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_TOKEN: ${{ github.token }} + RELEASE_COMMIT: ${{ inputs.release_commit }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + git fetch --no-tags origin "$DEFAULT_BRANCH" + CONTROL_PLANE_COMMIT="$(git rev-parse "origin/$DEFAULT_BRANCH")" + if ! printf '%s' "$CONTROL_PLANE_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then + echo "default-branch HEAD is unavailable for package publication dispatch" >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$RELEASE_COMMIT" "$CONTROL_PLANE_COMMIT"; then + echo "release commit must be an ancestor of the current default branch" >&2 + exit 1 + fi + gh workflow run publish-pypi.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref "$DEFAULT_BRANCH" \ + -f release_tag="v$RELEASE_VERSION" \ + -f release_commit="$RELEASE_COMMIT" \ + -f control_plane_commit="$CONTROL_PLANE_COMMIT" diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index f8ab04b865..8dfacff5ab 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -50,7 +50,7 @@ jobs: # Fails OPEN: an unreadable, empty, or truncated file list scans everything. # The gate lives inside this job as a step-level guard (one runner, not two). if: github.event.action != 'closed' - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/sast-semgrep.yml@refs/heads/main' && '{"group":"CWL MCP remediation","labels":["self-hosted","Linux","X64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}') || fromJSON('["ubuntu-24.04"]') }} permissions: contents: read pull-requests: read diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index e04d7bf8f3..0021f13815 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -71,7 +71,7 @@ jobs: # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. if: github.event.action != 'closed' - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/security-scan.yml@refs/heads/main' && '{"group":"CWL MCP remediation","labels":["self-hosted","Linux","X64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}') || fromJSON('["ubuntu-24.04"]') }} timeout-minutes: 5 permissions: contents: read @@ -126,7 +126,7 @@ jobs: osv-scan: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.deps == 'true' - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/security-scan.yml@refs/heads/main' && '{"group":"CWL MCP remediation","labels":["self-hosted","Linux","X64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}') || fromJSON('["ubuntu-24.04"]') }} timeout-minutes: 25 permissions: actions: read @@ -352,7 +352,7 @@ jobs: dependency-review: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.deps == 'true' - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/security-scan.yml@refs/heads/main' && '{"group":"CWL MCP remediation","labels":["self-hosted","Linux","X64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}') || fromJSON('["ubuntu-24.04"]') }} permissions: contents: read pull-requests: read @@ -436,7 +436,7 @@ jobs: gitleaks: name: gitleaks (secret scan) if: github.event.action != 'closed' && github.repository == 'ContextualWisdomLab/.github' - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/security-scan.yml@refs/heads/main' && '{"group":"CWL MCP remediation","labels":["self-hosted","Linux","X64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}') || fromJSON('["ubuntu-24.04"]') }} permissions: contents: read security-events: write @@ -526,7 +526,7 @@ jobs: trivy-fs: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.code == 'true' - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/security-scan.yml@refs/heads/main' && '{"group":"CWL MCP remediation","labels":["self-hosted","Linux","X64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}') || fromJSON('["ubuntu-24.04"]') }} permissions: contents: read security-events: write @@ -633,7 +633,7 @@ jobs: scorecard: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.code == 'true' - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/security-scan.yml@refs/heads/main' && '{"group":"CWL MCP remediation","labels":["self-hosted","Linux","X64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}') || fromJSON('["ubuntu-24.04"]') }} # SOFT: posture findings are unrelated to the PR diff, so never block merge. continue-on-error: true permissions: diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py index e380c81253..c1fd52c0e5 100644 --- a/tests/test_fast_mlsirm_self_hosted_workflows.py +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -62,3 +62,16 @@ def test_opencode_gateway_config_keeps_reasoning_and_tools(): assert provider["options"] == {"baseURL": "https://litellm.poinnetworks.net/v1", "apiKey": "{env:LLM_GATEWAY_API_KEY}"} assert provider["models"]["auto"]["tool_call"] assert provider["models"]["auto"]["options"]["reasoningEffort"] == "high" + + +def test_hosted_only_release_legs_are_disabled_without_partial_publication(): + import yaml + root = Path(__file__).resolve().parents[1] + workflow = yaml.safe_load((root / '.github/workflows/fast-mlsirm-publish-pypi.yml').read_text()) + jobs = workflow['jobs'] + assert jobs['wheels']['if'] == '${{ false }}' + assert jobs['macos-x86-runtime']['if'] == '${{ false }}' + assert 'macos-x86-runtime' in jobs['reproducibility-record']['needs'] + assert 'reproducibility-record' in jobs['publish-pypi']['needs'] + assert 'release-admission' in jobs['publish-pypi']['needs'] + assert 'if' not in jobs['publish-pypi'] From e8d70155d8a0dfdca5ef8e5c7601971787db7c6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:25:16 +0900 Subject: [PATCH 07/16] ci(codeql): keep migrated branch revisions off hosted runners --- .github/workflows/codeql-pr.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 356244f7fc..2d61c3a9fe 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -60,7 +60,7 @@ jobs: name: Detect CodeQL languages # Draft PRs get no runner; ready_for_review re-runs this on the same head. if: (github.event.action != 'closed') && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} + runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}' || '"ubuntu-24.04"')) }} timeout-minutes: 5 permissions: contents: read @@ -164,7 +164,7 @@ jobs: # dependency exactly; the only case where it's genuinely skipped is a # closed PR, where this job being implicitly skipped too is fine because # closed PRs need no required check. - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} + runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}' || '"ubuntu-24.04"')) }} # Verdict reads have exceeded five minutes; retain the ten-minute control budget. timeout-minutes: 10 permissions: @@ -367,7 +367,7 @@ jobs: && github.event.pull_request.state != 'closed' && needs.detect-languages.result == 'success' && needs.detect-languages.outputs.code == 'true' - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} + runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}' || '"ubuntu-24.04"')) }} timeout-minutes: 5 permissions: contents: read From 5bc93fa8c9d37f3afa6cdb6395cd04a03880b0cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:26:46 +0900 Subject: [PATCH 08/16] ci: route fast-mlsirm reusable workflows to online self-hosted linux --- .github/workflows/codeql-pr.yml | 6 ++--- .github/workflows/fast-mlsirm-cflite_pr.yml | 3 +-- .github/workflows/fast-mlsirm-ci.yml | 18 +++++-------- .github/workflows/fast-mlsirm-codeql.yml | 6 ++--- .../fast-mlsirm-hourly-pr-governance.yml | 3 +-- .../workflows/fast-mlsirm-publish-pypi.yml | 27 +++++++------------ .../workflows/fast-mlsirm-pypi-gap-guard.yml | 3 +-- .github/workflows/fast-mlsirm-release-tag.yml | 3 +-- .../fast-mlsirm-statistical-studies.yml | 15 ++++------- .../test_fast_mlsirm_self_hosted_workflows.py | 3 +-- 10 files changed, 30 insertions(+), 57 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 2d61c3a9fe..0c6f3baf2e 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -60,7 +60,7 @@ jobs: name: Detect CodeQL languages # Draft PRs get no runner; ready_for_review re-runs this on the same head. if: (github.event.action != 'closed') && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}' || '"ubuntu-24.04"')) }} + runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"labels":["self-hosted","Linux","X64","cwlab","ubuntu-24.04"]}' || '"ubuntu-24.04"')) }} timeout-minutes: 5 permissions: contents: read @@ -164,7 +164,7 @@ jobs: # dependency exactly; the only case where it's genuinely skipped is a # closed PR, where this job being implicitly skipped too is fine because # closed PRs need no required check. - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}' || '"ubuntu-24.04"')) }} + runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"labels":["self-hosted","Linux","X64","cwlab","ubuntu-24.04"]}' || '"ubuntu-24.04"')) }} # Verdict reads have exceeded five minutes; retain the ten-minute control budget. timeout-minutes: 10 permissions: @@ -367,7 +367,7 @@ jobs: && github.event.pull_request.state != 'closed' && needs.detect-languages.result == 'success' && needs.detect-languages.outputs.code == 'true' - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}' || '"ubuntu-24.04"')) }} + runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"labels":["self-hosted","Linux","X64","cwlab","ubuntu-24.04"]}' || '"ubuntu-24.04"')) }} timeout-minutes: 5 permissions: contents: read diff --git a/.github/workflows/fast-mlsirm-cflite_pr.yml b/.github/workflows/fast-mlsirm-cflite_pr.yml index 63482da31a..fcd0096e86 100644 --- a/.github/workflows/fast-mlsirm-cflite_pr.yml +++ b/.github/workflows/fast-mlsirm-cflite_pr.yml @@ -10,8 +10,7 @@ jobs: PR: if: ${{ !github.event.pull_request.draft && github.event.action != 'closed' }} runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] permissions: contents: read security-events: write diff --git a/.github/workflows/fast-mlsirm-ci.yml b/.github/workflows/fast-mlsirm-ci.yml index 53d6324a85..4de3523c22 100644 --- a/.github/workflows/fast-mlsirm-ci.yml +++ b/.github/workflows/fast-mlsirm-ci.yml @@ -21,8 +21,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} name: python (${{ matrix.python-version }}) runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] strategy: fail-fast: false matrix: @@ -51,8 +50,7 @@ jobs: needs: python-matrix if: ${{ always() && (github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed')) }} runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] steps: - name: Require every CPython matrix leg run: | @@ -63,8 +61,7 @@ jobs: rust: if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: @@ -92,8 +89,7 @@ jobs: # non-execution allowlist entry. The full paper-design recovery study # remains in the scheduled/manual workflow. runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 240 env: WGPU_BACKEND: vulkan @@ -190,8 +186,7 @@ jobs: # dev extra); this job adds the Atheris coverage-guided harnesses with a # ~60s-per-target budget. Longer campaigns can be run nightly if desired. runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: @@ -241,8 +236,7 @@ jobs: package: if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: diff --git a/.github/workflows/fast-mlsirm-codeql.yml b/.github/workflows/fast-mlsirm-codeql.yml index a799a9191a..a6bf36db4b 100644 --- a/.github/workflows/fast-mlsirm-codeql.yml +++ b/.github/workflows/fast-mlsirm-codeql.yml @@ -16,8 +16,7 @@ jobs: # on PRs. Default setup remains the repository's code-scanning upload owner. name: Analyze (actions) runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] permissions: contents: read security-events: write @@ -44,8 +43,7 @@ jobs: if: github.event_name == 'workflow_dispatch' name: Analyze (python) runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] permissions: contents: read security-events: write diff --git a/.github/workflows/fast-mlsirm-hourly-pr-governance.yml b/.github/workflows/fast-mlsirm-hourly-pr-governance.yml index f72bc91bfc..8a6ef62fbb 100644 --- a/.github/workflows/fast-mlsirm-hourly-pr-governance.yml +++ b/.github/workflows/fast-mlsirm-hourly-pr-governance.yml @@ -16,8 +16,7 @@ jobs: governance-evidence: name: Hourly PR queue governance evidence runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 10 steps: - name: Check out reviewed source diff --git a/.github/workflows/fast-mlsirm-publish-pypi.yml b/.github/workflows/fast-mlsirm-publish-pypi.yml index 2c44cf87b7..268c8617eb 100644 --- a/.github/workflows/fast-mlsirm-publish-pypi.yml +++ b/.github/workflows/fast-mlsirm-publish-pypi.yml @@ -26,8 +26,7 @@ jobs: verify-release: name: verify release provenance runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 5 outputs: release_commit: ${{ steps.release-source.outputs.value }} @@ -151,8 +150,7 @@ jobs: name: sdist needs: verify-release runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 15 env: SOURCE_DATE_EPOCH: ${{ needs.verify-release.outputs.source_date_epoch }} @@ -389,8 +387,7 @@ jobs: name: wheels (${{ matrix.target }}, py${{ matrix.python-version }}) needs: [verify-release, sdist] runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 90 env: SOURCE_DATE_EPOCH: ${{ needs.verify-release.outputs.source_date_epoch }} @@ -697,8 +694,7 @@ jobs: name: macOS x86 runtime ${{ matrix.python-version }} needs: [verify-release, wheels] runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 20 permissions: contents: read @@ -785,8 +781,7 @@ jobs: name: reproducibility record needs: [verify-release, sdist, wheels, macos-x86-runtime] runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 5 permissions: contents: read @@ -1091,8 +1086,7 @@ jobs: name: release admission needs: [verify-release, reproducibility-record, dependency-gate] runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 10 permissions: contents: read @@ -1565,8 +1559,7 @@ jobs: name: create tag and release after admission needs: [verify-release, release-admission] runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 10 permissions: contents: write @@ -1830,8 +1823,7 @@ jobs: name: attach release assets needs: [sdist, wheels, reproducibility-record, release-admission, create-tag-and-release] runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 10 permissions: contents: write @@ -1907,8 +1899,7 @@ jobs: name: publish to PyPI needs: [sdist, wheels, reproducibility-record, release-admission, create-tag-and-release] runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 15 environment: pypi permissions: diff --git a/.github/workflows/fast-mlsirm-pypi-gap-guard.yml b/.github/workflows/fast-mlsirm-pypi-gap-guard.yml index c21e261037..1cb514b47c 100644 --- a/.github/workflows/fast-mlsirm-pypi-gap-guard.yml +++ b/.github/workflows/fast-mlsirm-pypi-gap-guard.yml @@ -20,8 +20,7 @@ concurrency: jobs: recover-missing-pypi: runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 15 permissions: contents: read diff --git a/.github/workflows/fast-mlsirm-release-tag.yml b/.github/workflows/fast-mlsirm-release-tag.yml index fa804750ab..cba7b9eace 100644 --- a/.github/workflows/fast-mlsirm-release-tag.yml +++ b/.github/workflows/fast-mlsirm-release-tag.yml @@ -21,8 +21,7 @@ concurrency: jobs: publish-release-tag: runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 10 permissions: contents: read diff --git a/.github/workflows/fast-mlsirm-statistical-studies.yml b/.github/workflows/fast-mlsirm-statistical-studies.yml index 276c98f8d7..0f483f87a8 100644 --- a/.github/workflows/fast-mlsirm-statistical-studies.yml +++ b/.github/workflows/fast-mlsirm-statistical-studies.yml @@ -19,8 +19,7 @@ jobs: rust-ignored: name: Ignored Rust studies shard ${{ matrix.shard }} runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] # Large Monte Carlo recovery tests are real evidence, not a reason to # silently discard a shard after the helper's default 30-minute bound. timeout-minutes: 180 @@ -54,8 +53,7 @@ jobs: rust-pyo3-ignored: name: Ignored PyO3 studies runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -69,8 +67,7 @@ jobs: rust-recovery: name: Literature CPU recovery studies runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 120 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -103,8 +100,7 @@ jobs: grm-recovery: name: GRM 500-rep recovery study runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 120 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -135,8 +131,7 @@ jobs: gpu-recovery: name: Literature GPU recovery parity runs-on: - group: CWL CI isolated - labels: [self-hosted, Linux, X64] + labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] timeout-minutes: 90 env: WGPU_BACKEND: vulkan diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py index c1fd52c0e5..5fdabfcadf 100644 --- a/tests/test_fast_mlsirm_self_hosted_workflows.py +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -8,8 +8,7 @@ def test_fast_mlsirm_reusable_workflows_keep_isolated_runner_and_gate_contracts( text = (workflows / f"fast-mlsirm-{name}.yml").read_text() assert "workflow_call:" in text assert "ubuntu-latest" not in text and "runs-on: ubuntu" not in text - assert "group: CWL CI isolated" in text - assert "labels: [self-hosted, Linux, X64]" in text + assert "labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04]" in text assert f"group: central-fast-mlsirm-{name}-" in text ci = (workflows / "fast-mlsirm-ci.yml").read_text() assert 'python-version: ["3.12", "3.14"]' in ci From f47dbe2ddb388ffe7a31b0dc15e1ea8063772202 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:27:13 +0900 Subject: [PATCH 09/16] Revert "ci: route fast-mlsirm reusable workflows to online self-hosted linux" This reverts commit 5bc93fa8c9d37f3afa6cdb6395cd04a03880b0cd. --- .github/workflows/codeql-pr.yml | 6 ++--- .github/workflows/fast-mlsirm-cflite_pr.yml | 3 ++- .github/workflows/fast-mlsirm-ci.yml | 18 ++++++++----- .github/workflows/fast-mlsirm-codeql.yml | 6 +++-- .../fast-mlsirm-hourly-pr-governance.yml | 3 ++- .../workflows/fast-mlsirm-publish-pypi.yml | 27 ++++++++++++------- .../workflows/fast-mlsirm-pypi-gap-guard.yml | 3 ++- .github/workflows/fast-mlsirm-release-tag.yml | 3 ++- .../fast-mlsirm-statistical-studies.yml | 15 +++++++---- .../test_fast_mlsirm_self_hosted_workflows.py | 3 ++- 10 files changed, 57 insertions(+), 30 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 0c6f3baf2e..2d61c3a9fe 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -60,7 +60,7 @@ jobs: name: Detect CodeQL languages # Draft PRs get no runner; ready_for_review re-runs this on the same head. if: (github.event.action != 'closed') && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"labels":["self-hosted","Linux","X64","cwlab","ubuntu-24.04"]}' || '"ubuntu-24.04"')) }} + runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}' || '"ubuntu-24.04"')) }} timeout-minutes: 5 permissions: contents: read @@ -164,7 +164,7 @@ jobs: # dependency exactly; the only case where it's genuinely skipped is a # closed PR, where this job being implicitly skipped too is fine because # closed PRs need no required check. - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"labels":["self-hosted","Linux","X64","cwlab","ubuntu-24.04"]}' || '"ubuntu-24.04"')) }} + runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}' || '"ubuntu-24.04"')) }} # Verdict reads have exceeded five minutes; retain the ten-minute control budget. timeout-minutes: 10 permissions: @@ -367,7 +367,7 @@ jobs: && github.event.pull_request.state != 'closed' && needs.detect-languages.result == 'success' && needs.detect-languages.outputs.code == 'true' - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"labels":["self-hosted","Linux","X64","cwlab","ubuntu-24.04"]}' || '"ubuntu-24.04"')) }} + runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && '{"group":"CWL CI isolated","labels":["self-hosted","Linux","X64"]}' || '"ubuntu-24.04"')) }} timeout-minutes: 5 permissions: contents: read diff --git a/.github/workflows/fast-mlsirm-cflite_pr.yml b/.github/workflows/fast-mlsirm-cflite_pr.yml index fcd0096e86..63482da31a 100644 --- a/.github/workflows/fast-mlsirm-cflite_pr.yml +++ b/.github/workflows/fast-mlsirm-cflite_pr.yml @@ -10,7 +10,8 @@ jobs: PR: if: ${{ !github.event.pull_request.draft && github.event.action != 'closed' }} runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] permissions: contents: read security-events: write diff --git a/.github/workflows/fast-mlsirm-ci.yml b/.github/workflows/fast-mlsirm-ci.yml index 4de3523c22..53d6324a85 100644 --- a/.github/workflows/fast-mlsirm-ci.yml +++ b/.github/workflows/fast-mlsirm-ci.yml @@ -21,7 +21,8 @@ jobs: if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} name: python (${{ matrix.python-version }}) runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] strategy: fail-fast: false matrix: @@ -50,7 +51,8 @@ jobs: needs: python-matrix if: ${{ always() && (github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed')) }} runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] steps: - name: Require every CPython matrix leg run: | @@ -61,7 +63,8 @@ jobs: rust: if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: @@ -89,7 +92,8 @@ jobs: # non-execution allowlist entry. The full paper-design recovery study # remains in the scheduled/manual workflow. runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 240 env: WGPU_BACKEND: vulkan @@ -186,7 +190,8 @@ jobs: # dev extra); this job adds the Atheris coverage-guided harnesses with a # ~60s-per-target budget. Longer campaigns can be run nightly if desired. runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: @@ -236,7 +241,8 @@ jobs: package: if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }} runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: diff --git a/.github/workflows/fast-mlsirm-codeql.yml b/.github/workflows/fast-mlsirm-codeql.yml index a6bf36db4b..a799a9191a 100644 --- a/.github/workflows/fast-mlsirm-codeql.yml +++ b/.github/workflows/fast-mlsirm-codeql.yml @@ -16,7 +16,8 @@ jobs: # on PRs. Default setup remains the repository's code-scanning upload owner. name: Analyze (actions) runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] permissions: contents: read security-events: write @@ -43,7 +44,8 @@ jobs: if: github.event_name == 'workflow_dispatch' name: Analyze (python) runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] permissions: contents: read security-events: write diff --git a/.github/workflows/fast-mlsirm-hourly-pr-governance.yml b/.github/workflows/fast-mlsirm-hourly-pr-governance.yml index 8a6ef62fbb..f72bc91bfc 100644 --- a/.github/workflows/fast-mlsirm-hourly-pr-governance.yml +++ b/.github/workflows/fast-mlsirm-hourly-pr-governance.yml @@ -16,7 +16,8 @@ jobs: governance-evidence: name: Hourly PR queue governance evidence runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 10 steps: - name: Check out reviewed source diff --git a/.github/workflows/fast-mlsirm-publish-pypi.yml b/.github/workflows/fast-mlsirm-publish-pypi.yml index 268c8617eb..2c44cf87b7 100644 --- a/.github/workflows/fast-mlsirm-publish-pypi.yml +++ b/.github/workflows/fast-mlsirm-publish-pypi.yml @@ -26,7 +26,8 @@ jobs: verify-release: name: verify release provenance runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 5 outputs: release_commit: ${{ steps.release-source.outputs.value }} @@ -150,7 +151,8 @@ jobs: name: sdist needs: verify-release runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 15 env: SOURCE_DATE_EPOCH: ${{ needs.verify-release.outputs.source_date_epoch }} @@ -387,7 +389,8 @@ jobs: name: wheels (${{ matrix.target }}, py${{ matrix.python-version }}) needs: [verify-release, sdist] runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 90 env: SOURCE_DATE_EPOCH: ${{ needs.verify-release.outputs.source_date_epoch }} @@ -694,7 +697,8 @@ jobs: name: macOS x86 runtime ${{ matrix.python-version }} needs: [verify-release, wheels] runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 20 permissions: contents: read @@ -781,7 +785,8 @@ jobs: name: reproducibility record needs: [verify-release, sdist, wheels, macos-x86-runtime] runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 5 permissions: contents: read @@ -1086,7 +1091,8 @@ jobs: name: release admission needs: [verify-release, reproducibility-record, dependency-gate] runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 10 permissions: contents: read @@ -1559,7 +1565,8 @@ jobs: name: create tag and release after admission needs: [verify-release, release-admission] runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 10 permissions: contents: write @@ -1823,7 +1830,8 @@ jobs: name: attach release assets needs: [sdist, wheels, reproducibility-record, release-admission, create-tag-and-release] runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 10 permissions: contents: write @@ -1899,7 +1907,8 @@ jobs: name: publish to PyPI needs: [sdist, wheels, reproducibility-record, release-admission, create-tag-and-release] runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 15 environment: pypi permissions: diff --git a/.github/workflows/fast-mlsirm-pypi-gap-guard.yml b/.github/workflows/fast-mlsirm-pypi-gap-guard.yml index 1cb514b47c..c21e261037 100644 --- a/.github/workflows/fast-mlsirm-pypi-gap-guard.yml +++ b/.github/workflows/fast-mlsirm-pypi-gap-guard.yml @@ -20,7 +20,8 @@ concurrency: jobs: recover-missing-pypi: runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 15 permissions: contents: read diff --git a/.github/workflows/fast-mlsirm-release-tag.yml b/.github/workflows/fast-mlsirm-release-tag.yml index cba7b9eace..fa804750ab 100644 --- a/.github/workflows/fast-mlsirm-release-tag.yml +++ b/.github/workflows/fast-mlsirm-release-tag.yml @@ -21,7 +21,8 @@ concurrency: jobs: publish-release-tag: runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 10 permissions: contents: read diff --git a/.github/workflows/fast-mlsirm-statistical-studies.yml b/.github/workflows/fast-mlsirm-statistical-studies.yml index 0f483f87a8..276c98f8d7 100644 --- a/.github/workflows/fast-mlsirm-statistical-studies.yml +++ b/.github/workflows/fast-mlsirm-statistical-studies.yml @@ -19,7 +19,8 @@ jobs: rust-ignored: name: Ignored Rust studies shard ${{ matrix.shard }} runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] # Large Monte Carlo recovery tests are real evidence, not a reason to # silently discard a shard after the helper's default 30-minute bound. timeout-minutes: 180 @@ -53,7 +54,8 @@ jobs: rust-pyo3-ignored: name: Ignored PyO3 studies runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -67,7 +69,8 @@ jobs: rust-recovery: name: Literature CPU recovery studies runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 120 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -100,7 +103,8 @@ jobs: grm-recovery: name: GRM 500-rep recovery study runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 120 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -131,7 +135,8 @@ jobs: gpu-recovery: name: Literature GPU recovery parity runs-on: - labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04] + group: CWL CI isolated + labels: [self-hosted, Linux, X64] timeout-minutes: 90 env: WGPU_BACKEND: vulkan diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py index 5fdabfcadf..c1fd52c0e5 100644 --- a/tests/test_fast_mlsirm_self_hosted_workflows.py +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -8,7 +8,8 @@ def test_fast_mlsirm_reusable_workflows_keep_isolated_runner_and_gate_contracts( text = (workflows / f"fast-mlsirm-{name}.yml").read_text() assert "workflow_call:" in text assert "ubuntu-latest" not in text and "runs-on: ubuntu" not in text - assert "labels: [self-hosted, Linux, X64, cwlab, ubuntu-24.04]" in text + assert "group: CWL CI isolated" in text + assert "labels: [self-hosted, Linux, X64]" in text assert f"group: central-fast-mlsirm-{name}-" in text ci = (workflows / "fast-mlsirm-ci.yml").read_text() assert 'python-version: ["3.12", "3.14"]' in ci From 740a206a856be85a2a8c184f6d0e820d1f1c1d5f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:30:48 +0900 Subject: [PATCH 10/16] fix(ci): preserve free review boundary and runner contracts Remove the unrelated public LiteLLM auto routes from Noema and OpenCode so required model-backed review remains fail-closed through contextual-orchestrator/orchestrator/free. Preserve the concurrent fast-mlsirm runner and release work, and update the exact runner-routing contracts for central main, isolated .github/fast-mlsirm PRs, and the Ubuntu 24.04 fallback. --- .github/workflows/noema-review.yml | 42 ++++----------- .../workflows/opencode-review-dispatch.yml | 49 ++--------------- tests/test_codeql_pr_workflow_contract.py | 16 +++--- tests/test_docs_only_pr_runner_admission.py | 29 ++++++++-- .../test_fast_mlsirm_self_hosted_workflows.py | 48 ----------------- ...t_required_review_runner_image_contract.py | 9 ++-- ...required_security_runner_image_contract.py | 53 +++++++++++++++---- ...d_codeql_dispatch_runner_image_contract.py | 12 ++++- 8 files changed, 109 insertions(+), 149 deletions(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 6baa4e1ff9..08ea600538 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -853,24 +853,9 @@ jobs: python-version: "3.12" update-environment: false - - name: Select public review gateway - id: llm_gateway - if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' - env: - LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} - REQUIRE_ZDR: ${{ steps.target_visibility.outputs.require_zdr }} - run: | - set -euo pipefail - # Private targets retain the verified ZDR sidecar route. - if [ "$REQUIRE_ZDR" = "false" ] && [ -n "${LLM_GATEWAY_API_KEY:-}" ]; then - echo "enabled=true" >>"$GITHUB_OUTPUT" - else - echo "enabled=false" >>"$GITHUB_OUTPUT" - fi - - name: Provision contextual-orchestrator review sidecar id: noema_sidecar - if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' && steps.llm_gateway.outputs.enabled != 'true' + if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' env: SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} @@ -927,8 +912,6 @@ jobs: if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' id: noema_prepare env: - LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} - USE_LLM_GATEWAY: ${{ steps.llm_gateway.outputs.enabled }} GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app' || 'noema-review-app-oidc' }} NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_token.outputs['app-slug']) || '' }} @@ -945,22 +928,15 @@ jobs: echo "::error::Noema reviewer credential selection succeeded but no token was minted; review cannot prepare a verdict." exit 1 fi - if [ "${USE_LLM_GATEWAY:-false}" = "true" ]; then - test -n "${LLM_GATEWAY_API_KEY:-}" - export NOEMA_LLM_API_URL="https://litellm.poinnetworks.net/v1/chat/completions" - export NOEMA_LLM_MODEL="auto" - export NOEMA_LLM_API_KEY="$LLM_GATEWAY_API_KEY" - else - if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-}" ]; then - echo "::error::A verified review route must be provisioned before Noema LLM review." - exit 1 - fi - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" - export NOEMA_LLM_API_URL="${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/v1/chat/completions" - export NOEMA_LLM_MODEL="orchestrator/free" - export NOEMA_LLM_API_KEY="${CONTEXTUAL_ORCHESTRATOR_TOKEN}" - export NOEMA_LLM_VIA_ORCHESTRATOR=1 + if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-}" ]; then + echo "::error::contextual-orchestrator review sidecar must be provisioned before Noema LLM review." + exit 1 fi + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + export NOEMA_LLM_API_URL="${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/v1/chat/completions" + export NOEMA_LLM_MODEL="orchestrator/free" + export NOEMA_LLM_API_KEY="${CONTEXTUAL_ORCHESTRATOR_TOKEN}" + export NOEMA_LLM_VIA_ORCHESTRATOR=1 verdict_file="${RUNNER_TEMP}/noema-verdict-envelope.json" rm -f "$verdict_file" python3 "$GITHUB_WORKSPACE/.github/actions/noema-review/two_phase.py" --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" --expected-head "$EXPECTED_HEAD_SHA" --prepare-verdict-file "$verdict_file" diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 1c3514d232..09bbf8181a 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -2655,21 +2655,7 @@ jobs: python-version: "3.12" update-environment: false - - name: Select public review gateway - id: llm_gateway - env: - LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} - IS_PRIVATE: ${{ needs.validate-pr-metadata.outputs.is_private }} - run: | - set -euo pipefail - if [ "$IS_PRIVATE" = "false" ] && [ -n "${LLM_GATEWAY_API_KEY:-}" ]; then - echo "enabled=true" >>"$GITHUB_OUTPUT" - else - echo "enabled=false" >>"$GITHUB_OUTPUT" - fi - - name: Provision contextual-orchestrator review sidecar - if: steps.llm_gateway.outputs.enabled != 'true' env: SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} @@ -3766,7 +3752,6 @@ jobs: - name: Prepare isolated OpenCode review workspace env: - USE_LLM_GATEWAY: ${{ steps.llm_gateway.outputs.enabled }} OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md @@ -4239,22 +4224,6 @@ jobs: echo '::error::Generated isolated opencode.jsonc is missing the contextual-orchestrator gateway route.' exit 1 fi - if [ "${USE_LLM_GATEWAY:-false}" = "true" ]; then - jq ' - .model = "contextual-orchestrator/auto" | - .small_model = "contextual-orchestrator/auto" | - .provider["contextual-orchestrator"].name = "LiteLLM" | - .provider["contextual-orchestrator"].options = { - "baseURL": "https://litellm.poinnetworks.net/v1", - "apiKey": "{env:LLM_GATEWAY_API_KEY}" - } | - .provider["contextual-orchestrator"].models = { - "auto": .provider["contextual-orchestrator"].models["orchestrator/free"] - } | - .provider["contextual-orchestrator"].models.auto.name = "Auto" - ' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" >"$gateway_config" - mv -- "$gateway_config" "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" - fi printf 'Prepared isolated OpenCode review workspace: %s\n' "$OPENCODE_REVIEW_WORKDIR" - name: Run OpenCode PR Review model pool @@ -4262,12 +4231,10 @@ jobs: if: needs.coverage-evidence.result == 'success' continue-on-error: true env: - USE_LLM_GATEWAY: ${{ steps.llm_gateway.outputs.enabled }} - LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_CANDIDATES: ${{ steps.llm_gateway.outputs.enabled == 'true' && 'contextual-orchestrator/auto' || 'contextual-orchestrator/orchestrator/free' }} + OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/free" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. @@ -4305,9 +4272,7 @@ jobs: RUN_ATTEMPT: ${{ github.run_attempt }} run: | set -euo pipefail - if [ "${USE_LLM_GATEWAY:-false}" != "true" ]; then - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" - fi + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" set +e bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh" pool_status=$? @@ -4865,8 +4830,6 @@ jobs: # extensions for image validation or package/GPU builds plus API and # publication overhead. env: - USE_LLM_GATEWAY: ${{ steps.llm_gateway.outputs.enabled }} - LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} CHECK_LOOKUP_GH_TOKEN: ${{ github.token }} # The OpenCode app installation token is exchanged from api.opencode.ai @@ -4889,7 +4852,7 @@ jobs: COVERAGE_EVIDENCE_SUMMARY: ${{ needs.coverage-evidence.outputs.coverage_summary || 'Coverage evidence job did not run or did not publish coverage evidence.' }} OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head - MODEL: ${{ steps.llm_gateway.outputs.enabled == 'true' && 'contextual-orchestrator/auto' || 'contextual-orchestrator/orchestrator/free' }} + MODEL: contextual-orchestrator/orchestrator/free USE_GITHUB_TOKEN: "true" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" @@ -6251,10 +6214,8 @@ jobs: printf 'Skipping publish-step failed-check OpenCode diagnosis for central review-process self-repair; using collected current-head failed-check logs/SARIF fallback so the publish step stays bounded.\n' >&2 return 1 fi - if [ "${USE_LLM_GATEWAY:-false}" != "true" ]; then - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" - fi - if [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}${LLM_GATEWAY_API_KEY:-}" ]; then + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + if [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then return 1 fi if ! python3 "$GITHUB_WORKSPACE/scripts/ci/assert_opencode_reasoning_effort.py" \ diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 82569e89c7..e482535322 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -1147,18 +1147,22 @@ def test_codeql_moved_head_requires_forward_ancestry(tmp_path: Path) -> None: assert enforce.returncode != 0 -def test_codeql_control_routing_keeps_pr_workflows_hosted() -> None: - """Only the trusted main revision may request the restricted control group.""" +def test_codeql_control_routing_preserves_three_runner_trust_levels() -> None: + """Route trusted main, isolated product PRs, and other consumers separately.""" workflow = WORKFLOW_PATH.read_text(encoding="utf-8") selectors = [line.strip() for line in workflow.splitlines() if line.strip().startswith("runs-on:")] trusted = "ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main" assert len(selectors) == 3 for selector in selectors: assert f"fromJSON(github.workflow_ref == '{trusted}' && " in selector - choices = re.findall(r"'([^']*)'", selector) - assert choices[0] == trusted - assert json.loads(choices[1]) == {"group": "CWL central control", "labels": ["self-hosted", "linux", "x64"]} - assert json.loads(choices[2]) == "ubuntu-24.04" + assert '"group":"CWL central control"' in selector + assert '"labels":["self-hosted","linux","x64"]' in selector + assert "github.repository == 'ContextualWisdomLab/.github'" in selector + assert "github.repository == 'ContextualWisdomLab/fast-mlsirm'" in selector + assert '"group":"CWL CI isolated"' in selector + assert '"labels":["self-hosted","Linux","X64"]' in selector + assert "'\"ubuntu-24.04\"'" in selector + assert '"group":"CWL MCP remediation"' not in selector def test_codeql_pr_rejects_invalid_required_run_time(tmp_path: Path) -> None: diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index a0f691e2a2..57a4df3040 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -17,8 +17,8 @@ `docs/doctoring/required-workflow-path-filter-boundary.md`. See also `tests/test_required_security_runner_image_contract.py` and -`tests/test_required_review_runner_image_contract.py`, which pin the -`runs-on: ubuntu-24.04` counts these gate jobs add. +`tests/test_required_review_runner_image_contract.py`, which pin each gate's +supported hosted fallback and trusted self-hosted routing boundary. """ from __future__ import annotations @@ -139,8 +139,19 @@ def test_gate_jobs_use_supported_runner_allocation(): elif filename == "noema-review.yml": assert "endsWith(github.workflow_ref, '@refs/heads/main')" in block, filename assert "fromJSON('[\"ubuntu-24.04\"]')" in block, filename + elif filename == "security-scan.yml": + assert "github.repository == 'ContextualWisdomLab/.github'" in block + assert "github.repository == 'ContextualWisdomLab/fast-mlsirm'" in block + assert ( + "github.workflow_ref == " + "'ContextualWisdomLab/.github/.github/workflows/" + "security-scan.yml@refs/heads/main'" + ) in block + assert '"group":"CWL MCP remediation"' in block + assert '"group":"CWL CI isolated"' in block + assert "fromJSON('[\"ubuntu-24.04\"]')" in block else: - assert "runs-on: ubuntu-24.04" in block, filename + raise AssertionError(f"unclassified runner contract: {filename}") assert "runs-on: ubuntu-latest" not in block, filename @@ -250,7 +261,17 @@ def test_sast_semgrep_folds_the_gate_into_its_single_consumer_at_step_level(): assert "changed-scope:" not in workflow assert "needs: changed-scope" not in workflow assert "needs.changed-scope" not in workflow - assert workflow.count("runs-on: ubuntu-24.04") == 1 + assert workflow.count("runs-on:") == 1 + assert "github.repository == 'ContextualWisdomLab/.github'" in workflow + assert "github.repository == 'ContextualWisdomLab/fast-mlsirm'" in workflow + assert ( + "github.workflow_ref == " + "'ContextualWisdomLab/.github/.github/workflows/" + "sast-semgrep.yml@refs/heads/main'" + ) in workflow + assert '"group":"CWL MCP remediation"' in workflow + assert '"group":"CWL CI isolated"' in workflow + assert "fromJSON('[\"ubuntu-24.04\"]')" in workflow semgrep = _top_level_job_block(workflow, "semgrep") assert not re.search(r"(?m)^ needs:", semgrep) diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py index c1fd52c0e5..da3c2ead5f 100644 --- a/tests/test_fast_mlsirm_self_hosted_workflows.py +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -16,54 +16,6 @@ def test_fast_mlsirm_reusable_workflows_keep_isolated_runner_and_gate_contracts( assert 'test "${{ needs.python-matrix.result }}" = "success"' in ci assert "cargo test --locked --workspace" in ci assert "cargo test --locked --manifest-path crates/fast-mlsirm-py/Cargo.toml" in ci - - -def test_noema_uses_litellm_auto_for_public_reviews_and_keeps_private_sidecar(): - root = Path(__file__).resolve().parents[1] - text = (root / ".github" / "workflows" / "noema-review.yml").read_text() - assert "https://litellm.poinnetworks.net/v1/chat/completions" in text - assert 'export NOEMA_LLM_MODEL="auto"' in text - assert "secrets.LLM_GATEWAY_API_KEY" in text - assert "Private targets retain the verified ZDR sidecar route." in text - assert "steps.llm_gateway.outputs.enabled != 'true'" in text - - -def test_public_gateway_selection_rejects_private_unknown_and_missing_credentials(tmp_path): - import os - import subprocess - import textwrap - - root = Path(__file__).resolve().parents[1] - for filename, flag in (("noema-review.yml", "REQUIRE_ZDR"), ("opencode-review-dispatch.yml", "IS_PRIVATE")): - text = (root / ".github" / "workflows" / filename).read_text() - step = text.split(" - name: Select public review gateway\n", 1)[1].split("\n - name:", 1)[0] - command = textwrap.dedent(step.split(" run: |\n", 1)[1]) - for visibility, key, enabled in (("false", "test-placeholder", True), ("true", "test-placeholder", False), ("", "test-placeholder", False), ("false", "", False)): - output = tmp_path / "output" - output.write_text("") - env = {**os.environ, flag: visibility, "LLM_GATEWAY_API_KEY": key, "GITHUB_OUTPUT": str(output)} - subprocess.run(["bash", "-c", command], env=env, check=True, capture_output=True) - assert output.read_text() == f"enabled={str(enabled).lower()}\n" - - -def test_opencode_gateway_config_keeps_reasoning_and_tools(): - import json - import subprocess - - root = Path(__file__).resolve().parents[1] - text = (root / ".github" / "workflows" / "opencode-review-dispatch.yml").read_text() - start = text.index(' jq \'\n .model = "contextual-orchestrator/auto"') - jq_filter = text[start:].split("jq '", 1)[1].split("' ", 1)[0] - original = {"provider": {"contextual-orchestrator": {"models": {"orchestrator/free": {"tool_call": True, "reasoning": True, "options": {"reasoningEffort": "high"}}}}}} - completed = subprocess.run(["jq", jq_filter], input=json.dumps(original), text=True, capture_output=True, check=True) - config = json.loads(completed.stdout) - assert config["model"] == "contextual-orchestrator/auto" - provider = config["provider"]["contextual-orchestrator"] - assert provider["options"] == {"baseURL": "https://litellm.poinnetworks.net/v1", "apiKey": "{env:LLM_GATEWAY_API_KEY}"} - assert provider["models"]["auto"]["tool_call"] - assert provider["models"]["auto"]["options"]["reasoningEffort"] == "high" - - def test_hosted_only_release_legs_are_disabled_without_partial_publication(): import yaml root = Path(__file__).resolve().parents[1] diff --git a/tests/test_required_review_runner_image_contract.py b/tests/test_required_review_runner_image_contract.py index ab7ede7aa4..7f250a0c26 100644 --- a/tests/test_required_review_runner_image_contract.py +++ b/tests/test_required_review_runner_image_contract.py @@ -95,10 +95,13 @@ def test_opencode_review_dispatch_uses_explicit_supported_image(self) -> None: unittest.main() -def test_codeql_pr_routes_trusted_main_to_control_and_pr_revisions_to_hosted() -> None: - """Separate short metadata work from model work without granting PR runner access.""" +def test_codeql_pr_routes_trusted_main_isolated_product_and_hosted_consumers() -> None: + """Preserve three runner trust levels without granting control-runner access.""" workflow = Path(".github/workflows/codeql-pr.yml").read_text() assert workflow.count('"group":"CWL central control"') == 3 assert workflow.count("github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main'") == 3 - assert workflow.count("|| '\"ubuntu-24.04\"'") == 3 + assert workflow.count("github.repository == 'ContextualWisdomLab/.github'") == 3 + assert workflow.count("github.repository == 'ContextualWisdomLab/fast-mlsirm'") == 3 + assert workflow.count('"group":"CWL CI isolated"') == 3 + assert workflow.count("'\"ubuntu-24.04\"'") == 3 assert '"group":"CWL MCP remediation"' not in workflow diff --git a/tests/test_required_security_runner_image_contract.py b/tests/test_required_security_runner_image_contract.py index d20c0c3a98..9fba97ab6a 100644 --- a/tests/test_required_security_runner_image_contract.py +++ b/tests/test_required_security_runner_image_contract.py @@ -11,20 +11,52 @@ class RequiredSecurityRunnerImageContract(unittest.TestCase): - """Keep required security jobs off the observed starved floating image.""" + """Keep security jobs on isolated runners where required, with a fixed fallback.""" + + def assert_runner_routing_contract( + self, + workflow: str, + *, + workflow_name: str, + expected_jobs: int, + ) -> None: + """Require trusted-main, isolated-product, and hosted-fallback routes.""" + self.assertNotIn("runs-on: ubuntu-latest", workflow) + runner_lines = [ + line.strip() + for line in workflow.splitlines() + if line.strip().startswith("runs-on:") + ] + self.assertEqual(len(runner_lines), expected_jobs) + required_fragments = ( + "github.repository == 'ContextualWisdomLab/.github'", + "github.repository == 'ContextualWisdomLab/fast-mlsirm'", + "github.workflow_ref == " + f"'ContextualWisdomLab/.github/.github/workflows/{workflow_name}" + "@refs/heads/main'", + '"group":"CWL MCP remediation"', + '"group":"CWL CI isolated"', + "fromJSON('[\"ubuntu-24.04\"]')", + ) + for runner_line in runner_lines: + for fragment in required_fragments: + self.assertIn(fragment, runner_line) def test_security_scan_uses_explicit_supported_image(self) -> None: - """Require every Security Scan job to use explicit Ubuntu 24.04. + """Require each Security Scan job to preserve all three runner routes. - 6, not 5: the document-scope-independent Gitleaks PR gate joined the - five existing required security jobs on this image. + Six jobs route fast-mlsirm PR work to the isolated group, central-main + work to the remediation group, and every other consumer to Ubuntu 24.04. """ workflow = SECURITY_SCAN.read_text(encoding="utf-8") - self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 6) + self.assert_runner_routing_contract( + workflow, + workflow_name="security-scan.yml", + expected_jobs=6, + ) def test_sast_semgrep_uses_explicit_supported_image(self) -> None: - """Require the SAST Semgrep job to use explicit Ubuntu 24.04. + """Require the SAST Semgrep job to preserve all three runner routes. `#1656` removed the sibling `cancel-closed-pr-runs` no-op job (it only duplicated PR-stable workflow concurrency), leaving one runner @@ -35,8 +67,11 @@ def test_sast_semgrep_uses_explicit_supported_image(self) -> None: `semgrep` job as a step-level guard (one consumer, one runner). """ workflow = SAST_SEMGREP.read_text(encoding="utf-8") - self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 1) + self.assert_runner_routing_contract( + workflow, + workflow_name="sast-semgrep.yml", + expected_jobs=1, + ) if __name__ == "__main__": diff --git a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py index 42efce621e..b1f02dcd98 100644 --- a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py +++ b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py @@ -49,7 +49,7 @@ def test_hourly_review_repair_uses_explicit_supported_image(self) -> None: self.assertIn("labels: [self-hosted, linux, x64]", workflow) def test_codeql_pr_uses_explicit_supported_image(self) -> None: - """Require trusted-main control routing and Ubuntu fallback for all three jobs.""" + """Require control, isolated-product, and Ubuntu routes for all three jobs.""" workflow = CODEQL_PR.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) selectors = [ @@ -64,7 +64,15 @@ def test_codeql_pr_uses_explicit_supported_image(self) -> None: ) self.assertIn('"group":"CWL central control"', selector) self.assertIn('"labels":["self-hosted","linux","x64"]', selector) - self.assertIn("|| '\"ubuntu-24.04\"'", selector) + self.assertIn("github.repository == 'ContextualWisdomLab/.github'", selector) + self.assertIn( + "github.repository == 'ContextualWisdomLab/fast-mlsirm'", + selector, + ) + self.assertIn('"group":"CWL CI isolated"', selector) + self.assertIn('"labels":["self-hosted","Linux","X64"]', selector) + self.assertIn("'\"ubuntu-24.04\"'", selector) + self.assertNotIn('"group":"CWL MCP remediation"', selector) def test_codeql_scan_dispatch_uses_explicit_supported_image(self) -> None: """Require validation, scan, and attempt wake jobs in the dedicated group.""" From 73db3e74849e9b461292fb4ab53eacf179d61c43 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:35:11 +0900 Subject: [PATCH 11/16] fix(release): fail closed while publication is unavailable Make the intentionally disabled release path return a failing job, scope the dependency gate to its five declared provider credentials, and propagate authenticated GitHub release inventory failures. The workflow contract test no longer introduces an undeclared PyYAML dependency. --- .../workflows/fast-mlsirm-publish-pypi.yml | 17 ++++- .../workflows/fast-mlsirm-pypi-gap-guard.yml | 12 ++-- .../test_fast_mlsirm_self_hosted_workflows.py | 63 ++++++++++++++----- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/.github/workflows/fast-mlsirm-publish-pypi.yml b/.github/workflows/fast-mlsirm-publish-pypi.yml index 2c44cf87b7..793e3d7d33 100644 --- a/.github/workflows/fast-mlsirm-publish-pypi.yml +++ b/.github/workflows/fast-mlsirm-publish-pypi.yml @@ -382,6 +382,16 @@ jobs: name: repro-digest-sdist path: repro-digest + publication-disabled: + name: wheel legs are disabled; this release was not published + needs: [sdist] + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64] + timeout-minutes: 1 + steps: + - run: exit 1 + wheels: # Disabled until self-hosted macOS, Windows and ARM release legs are available. # Keep the full release matrix contract; downstream publication remains skipped. @@ -1081,7 +1091,12 @@ jobs: distribution_set_artifact_digest: ${{ needs.reproducibility-record.outputs.distribution_set_artifact_digest }} wheel_filename: ${{ needs.reproducibility-record.outputs.selected_wheel_filename }} sdist_filename: ${{ needs.reproducibility-record.outputs.selected_sdist_filename }} - secrets: inherit + secrets: + BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} + NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # R5b/R5c: nothing is tagged, released or published until the exact thirteen # files match both the reproducibility record and a successful same-run diff --git a/.github/workflows/fast-mlsirm-pypi-gap-guard.yml b/.github/workflows/fast-mlsirm-pypi-gap-guard.yml index c21e261037..b9071fdda4 100644 --- a/.github/workflows/fast-mlsirm-pypi-gap-guard.yml +++ b/.github/workflows/fast-mlsirm-pypi-gap-guard.yml @@ -40,6 +40,7 @@ jobs: env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} EVENT_NAME: ${{ github.event_name }} + GH_TOKEN: ${{ github.token }} RELEASE_TAG_HINT: ${{ github.event.release.tag_name }} run: | set -euo pipefail @@ -85,14 +86,15 @@ jobs: else # gh release list is scoped to this repository and avoids a broad # REST pagination loop for every org repo. - while IFS= read -r tag; do - [ -n "$tag" ] || continue - candidate_tags+=("$tag") - done < <( + release_tags="$( gh release list --repo "$GITHUB_REPOSITORY" --limit 100 \ --json tagName \ --jq '.[].tagName' - ) + )" + while IFS= read -r tag; do + [ -n "$tag" ] || continue + candidate_tags+=("$tag") + done <<< "$release_tags" fi : > gap_tags.txt diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py index da3c2ead5f..d55e4e10aa 100644 --- a/tests/test_fast_mlsirm_self_hosted_workflows.py +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -1,29 +1,62 @@ from pathlib import Path +ROOT = Path(__file__).resolve().parents[1] +WORKFLOWS = ROOT / ".github" / "workflows" + + +def workflow_text(name: str) -> str: + """Return one central fast-mlsirm workflow as UTF-8 text.""" + return (WORKFLOWS / name).read_text(encoding="utf-8") + + def test_fast_mlsirm_reusable_workflows_keep_isolated_runner_and_gate_contracts(): - root = Path(__file__).resolve().parents[1] - workflows = root / ".github" / "workflows" for name in ("ci", "codeql", "statistical-studies", "hourly-pr-governance", "pypi-gap-guard"): - text = (workflows / f"fast-mlsirm-{name}.yml").read_text() + text = workflow_text(f"fast-mlsirm-{name}.yml") assert "workflow_call:" in text assert "ubuntu-latest" not in text and "runs-on: ubuntu" not in text assert "group: CWL CI isolated" in text assert "labels: [self-hosted, Linux, X64]" in text assert f"group: central-fast-mlsirm-{name}-" in text - ci = (workflows / "fast-mlsirm-ci.yml").read_text() + ci = workflow_text("fast-mlsirm-ci.yml") assert 'python-version: ["3.12", "3.14"]' in ci assert 'test "${{ needs.python-matrix.result }}" = "success"' in ci assert "cargo test --locked --workspace" in ci assert "cargo test --locked --manifest-path crates/fast-mlsirm-py/Cargo.toml" in ci -def test_hosted_only_release_legs_are_disabled_without_partial_publication(): - import yaml - root = Path(__file__).resolve().parents[1] - workflow = yaml.safe_load((root / '.github/workflows/fast-mlsirm-publish-pypi.yml').read_text()) - jobs = workflow['jobs'] - assert jobs['wheels']['if'] == '${{ false }}' - assert jobs['macos-x86-runtime']['if'] == '${{ false }}' - assert 'macos-x86-runtime' in jobs['reproducibility-record']['needs'] - assert 'reproducibility-record' in jobs['publish-pypi']['needs'] - assert 'release-admission' in jobs['publish-pypi']['needs'] - assert 'if' not in jobs['publish-pypi'] + + +def test_disabled_release_legs_fail_the_caller_instead_of_reporting_success(): + """Expose the intentional release hold as a failing required job.""" + text = workflow_text("fast-mlsirm-publish-pypi.yml") + assert text.count(" if: ${{ false }}") == 2 + assert " publication-disabled:\n" in text + assert " needs: [sdist]\n" in text + assert "wheel legs are disabled; this release was not published" in text + assert " - run: exit 1\n" in text + + +def test_release_dependency_gate_passes_only_declared_provider_secrets(): + """Keep the PyPI credential outside the dependency-analysis boundary.""" + text = workflow_text("fast-mlsirm-publish-pypi.yml") + gate = text.split(" dependency-gate:\n", 1)[1].split("\n release-admission:\n", 1)[0] + assert "secrets: inherit" not in gate + for name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + assert f" {name}: ${{{{ secrets.{name} }}}}" in gate + assert "PIPY_TOKEN" not in gate + + +def test_gap_guard_fails_closed_when_release_inventory_cannot_be_read(): + """Authenticate gh and propagate release-list failures before gap decisions.""" + text = workflow_text("fast-mlsirm-pypi-gap-guard.yml") + inventory = text.split(" - name: Resolve control-plane commit and PyPI inventory\n", 1)[1] + inventory = inventory.split("\n - name: Dispatch publish-pypi", 1)[0] + assert " GH_TOKEN: ${{ github.token }}\n" in inventory + assert ' release_tags="$(' in inventory + assert ' done <<< "$release_tags"' in inventory + assert " done < <(" not in inventory From 8a5e912188c8644744bb352ef752d027b34b79d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 14:46:03 +0900 Subject: [PATCH 12/16] fix(release): keep provider credentials behind gateway --- .../workflows/fast-mlsirm-publish-pypi.yml | 18 +++++++------ tests/test_audit_org_codeql_coverage.py | 25 ++++++++++++++++--- .../test_fast_mlsirm_self_hosted_workflows.py | 11 ++++---- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/.github/workflows/fast-mlsirm-publish-pypi.yml b/.github/workflows/fast-mlsirm-publish-pypi.yml index 793e3d7d33..952b0f9777 100644 --- a/.github/workflows/fast-mlsirm-publish-pypi.yml +++ b/.github/workflows/fast-mlsirm-publish-pypi.yml @@ -383,14 +383,19 @@ jobs: path: repro-digest publication-disabled: - name: wheel legs are disabled; this release was not published + name: publication disabled (release runner matrix unavailable) needs: [sdist] runs-on: group: CWL CI isolated labels: [self-hosted, Linux, X64] timeout-minutes: 1 steps: - - run: exit 1 + - name: Fail closed while required release runners are unavailable + env: + RELEASE_TAG: ${{ inputs.release_tag }} + run: | + echo "::error::Release ${RELEASE_TAG} was not tagged, released, or published because the required macOS, Windows, and ARM self-hosted release legs are unavailable." + exit 1 wheels: # Disabled until self-hosted macOS, Windows and ARM release legs are available. @@ -1091,12 +1096,9 @@ jobs: distribution_set_artifact_digest: ${{ needs.reproducibility-record.outputs.distribution_set_artifact_digest }} wheel_filename: ${{ needs.reproducibility-record.outputs.selected_wheel_filename }} sdist_filename: ${{ needs.reproducibility-record.outputs.selected_sdist_filename }} - secrets: - BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} - NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} - NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # The pinned owner contract still declares direct provider credentials. + # Do not cross that legacy boundary: this release path remains fail-closed + # until an immutable gateway-token-only contract is released and pinned. # R5b/R5c: nothing is tagged, released or published until the exact thirteen # files match both the reproducibility record and a successful same-run diff --git a/tests/test_audit_org_codeql_coverage.py b/tests/test_audit_org_codeql_coverage.py index 0ffc8fa749..375958b060 100644 --- a/tests/test_audit_org_codeql_coverage.py +++ b/tests/test_audit_org_codeql_coverage.py @@ -35,9 +35,13 @@ def default_setup_scanning_nothing(name: str) -> dict: } -def covered_by_recent_analysis(name: str, *, days_ago: int = 1) -> dict: +def covered_by_recent_analysis( + name: str, *, days_ago: int = 1, reference_time: datetime = NOW +) -> dict: """Return a repository payload covered by a recent, successful CodeQL analysis.""" - created_at = (NOW - timedelta(days=days_ago)).isoformat().replace("+00:00", "Z") + created_at = (reference_time - timedelta(days=days_ago)).isoformat().replace( + "+00:00", "Z" + ) return { "name": name, "archived": False, @@ -263,7 +267,13 @@ def test_null_latest_analysis_is_not_coverage() -> None: def test_audit_codeql_coverage_defaults_now_to_current_time() -> None: - repositories = [covered_by_recent_analysis("DefaultNowRepo", days_ago=0)] + repositories = [ + covered_by_recent_analysis( + "DefaultNowRepo", + days_ago=0, + reference_time=datetime.now(timezone.utc), + ) + ] assert audit.audit_codeql_coverage(repositories) == [] @@ -302,7 +312,14 @@ def test_main_fail_path_reports_gaps_from_stdin(monkeypatch, capsys) -> None: def test_main_pass_path_reports_from_file_arg(tmp_path, capsys) -> None: payload_path = tmp_path / "repositories.json" payload_path.write_text( - json.dumps([covered_by_default_setup("ELUNVERA"), covered_by_recent_analysis("Orgmetra")]), + json.dumps( + [ + covered_by_default_setup("ELUNVERA"), + covered_by_recent_analysis( + "Orgmetra", reference_time=datetime.now(timezone.utc) + ), + ] + ), encoding="utf-8", ) diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py index d55e4e10aa..b49798db7b 100644 --- a/tests/test_fast_mlsirm_self_hosted_workflows.py +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -31,12 +31,13 @@ def test_disabled_release_legs_fail_the_caller_instead_of_reporting_success(): assert text.count(" if: ${{ false }}") == 2 assert " publication-disabled:\n" in text assert " needs: [sdist]\n" in text - assert "wheel legs are disabled; this release was not published" in text - assert " - run: exit 1\n" in text + assert "publication disabled (release runner matrix unavailable)" in text + assert "required macOS, Windows, and ARM self-hosted release legs" in text + assert " exit 1\n" in text -def test_release_dependency_gate_passes_only_declared_provider_secrets(): - """Keep the PyPI credential outside the dependency-analysis boundary.""" +def test_release_dependency_gate_does_not_cross_provider_credentials(): + """Keep provider credentials behind contextual-orchestrator's boundary.""" text = workflow_text("fast-mlsirm-publish-pypi.yml") gate = text.split(" dependency-gate:\n", 1)[1].split("\n release-admission:\n", 1)[0] assert "secrets: inherit" not in gate @@ -47,7 +48,7 @@ def test_release_dependency_gate_passes_only_declared_provider_secrets(): "OPENROUTER_API_KEY", "OPENAI_API_KEY", ): - assert f" {name}: ${{{{ secrets.{name} }}}}" in gate + assert name not in gate assert "PIPY_TOKEN" not in gate From 06053ba3f4b716423739a63c6b8f73bcf8c117be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 10 Oct 2026 21:47:43 +0900 Subject: [PATCH 13/16] fix(review): keep diagnostic artifact quota separate from Noema verdict --- .github/workflows/noema-review.yml | 3 ++- tests/test_noema_orchestrator_workflow_contract.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 4389bf19d6..4459dda08a 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -991,7 +991,8 @@ jobs: fi - name: Upload contextual-orchestrator sidecar evidence - if: always() && env.PR_NUMBER != '' + if: always() && env.PR_NUMBER != '' && steps.noema_sidecar.outcome != 'skipped' && steps.noema_sidecar.outcome != '' + continue-on-error: true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: noema-sidecar-evidence diff --git a/tests/test_noema_orchestrator_workflow_contract.py b/tests/test_noema_orchestrator_workflow_contract.py index 1c1e3696d7..50fb1a3ddc 100644 --- a/tests/test_noema_orchestrator_workflow_contract.py +++ b/tests/test_noema_orchestrator_workflow_contract.py @@ -702,13 +702,15 @@ def test_noema_review_job_has_no_job_level_timeout() -> None: def test_noema_review_retains_sanitized_sidecar_evidence_after_any_outcome() -> None: """Retain existing sanitized evidence on success, failure and cancellation. - A forced runner shutdown can still prevent upload; this contract only - removes the failure-only gate without adding raw logs or new files. + Upload only after provisioning was attempted. Diagnostic upload failure + must not replace a valid verdict or a model failure with a storage error. """ workflow = workflow_text("noema-review.yml") name = "Upload contextual-orchestrator sidecar evidence" step = workflow_step(workflow, name) - assert "if: always() && env.PR_NUMBER != ''" in step + assert "steps.noema_sidecar.outcome != 'skipped'" in step + assert "steps.noema_sidecar.outcome != ''" in step + assert "continue-on-error: true" in step strix_pin = re.search( r"actions/upload-artifact@([0-9a-f]{40})", workflow_text("strix.yml") ).group(1) From 7dad40e10704494659e0c1a702dd04d2cd3d0fa9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 11 Oct 2026 01:30:35 +0900 Subject: [PATCH 14/16] Fix fast-mlsirm CodeQL diagnostic quota handling --- .github/workflows/fast-mlsirm-codeql.yml | 14 ++++---------- docs/product-technical-gap-baseline.md | 2 ++ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/fast-mlsirm-codeql.yml b/.github/workflows/fast-mlsirm-codeql.yml index 4a0807452c..6829d2f9ab 100644 --- a/.github/workflows/fast-mlsirm-codeql.yml +++ b/.github/workflows/fast-mlsirm-codeql.yml @@ -32,11 +32,8 @@ jobs: with: upload: never output: codeql-results/actions - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: codeql-diagnostic-actions-${{ github.sha }} - path: codeql-results/actions/*.sarif - if-no-files-found: error + - name: Keep CodeQL diagnostics local to the job + run: test -s codeql-results/actions/actions.sarif analyze-python: # Python remains a no-upload diagnostic for explicit operator runs; @@ -60,8 +57,5 @@ jobs: with: upload: never output: codeql-results/python - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: codeql-diagnostic-python-${{ github.sha }} - path: codeql-results/python/*.sarif - if-no-files-found: error + - name: Keep CodeQL diagnostics local to the job + run: test -s codeql-results/python/python.sarif diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 25d6c727fd..c39bc9802e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3168,6 +3168,8 @@ or whether default-setup landed on it (and possibly others) through an unrelated - **`fast-mlsirm`** — false positive. `.github/workflows/codeql.yml` runs two real jobs (`analyze-actions` on every PR, `analyze-python` gated to `workflow_dispatch` only), and **both** `analyze` steps carry `with: upload: never`, with comments stating *"Default setup remains the repository's code-scanning upload owner"* and *"Default setup already owns ordinary Python code-scanning uploads."* Confirmed via a live job log (run `33754939454`, job `100646992008`, `2026-09-04T00:45Z`): `upload: never` present in the action's resolved input dump, `Exported results to SARIF` followed by no upload call, job concluded `success`. Deliberately engineered the opposite way from `contextual-orchestrator`'s fix (default-setup keeps ownership, the local workflow stays silent) rather than the way `contextual-orchestrator` was fixed (local workflow keeps ownership, default-setup disabled) — both are valid resolutions of the same conflict; this repository already had one in place. No fix needed. - **`scopeweave`** — no live conflict, but two dangling artifacts worth a light cleanup. The workflow with real `init`/`analyze` steps (`.github/workflows/codeql.yml`) is `disabled_manually`, so it never runs and cannot collide with default-setup today. A second, unrelated workflow entry — "CodeQL Required," id `335384625`, `.github/workflows/codeql-required.yml` — is registered `state: "active"` in the Actions API, but the file itself no longer exists on the `develop` default branch (`404` on direct content fetch); GitHub retains the workflow-run registration for a file that has since been deleted, so this entry can never actually trigger. Net effect: default-setup is the sole current CodeQL coverage source for this repository, matching item-41's own "zero coverage from any source" criterion at whatever point `codeql.yml` was disabled — not a misclassification, just a repository whose local workflow went inactive after (or independent of) the rollout. Not fixed in this pass: re-enabling the disabled `codeql.yml` would immediately recreate `contextual-orchestrator`'s exact conflict, so any future re-enable of that workflow must add `upload: never` (matching `fast-mlsirm`'s pattern) or disable default-setup first, whichever this repository's owner intends as the coverage source of record. +**2026-10-11 diagnostic-storage repair (ContextualWisdomLab/.github#2607):** fast-mlsirm #2073 CodeQL job `114230230294` failed at `2026-10-10T15:36:55Z` because Actions artifact storage quota prevented its diagnostic SARIF upload. The central reusable workflow keeps SARIF local and requires a nonempty result after analysis. Default setup retains code-scanning upload ownership; live success for this revision is still pending. + **The remaining 7 repositories** (`newsdom-api`, `keyverse`, `bandscope`, `mightyETL`, `litellm-patched-proxy`, `pg-erd-cloud`, `.github`) all returned `default-setup=not-configured` — no conflict is possible regardless of their local workflow's upload configuration. **Conclusion.** `contextual-orchestrator`'s conflict was an isolated incident, not a symptom of a broader misclassification in item-41's rollout (none of the 3 repositories found here with `default-setup=configured` alongside a local workflow were among that rollout's 23 targets) and not evidence of an org policy silently re-enabling default-setup on repositories that already had real coverage. Two of the three already carry a deliberate, working design for this exact conflict (`if: false` / `upload: never`) that predates or is independent of this audit — worth keeping as the reference pattern if this conflict resurfaces elsewhere, in preference to `contextual-orchestrator`'s "disable default-setup" fix when the local workflow does not yet have established real-coverage precedence. From 5e9936306faf7e74030ae9e662826175e8c63abb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 11 Oct 2026 01:38:37 +0900 Subject: [PATCH 15/16] Publish fast-mlsirm CodeQL SARIF without artifacts --- .github/workflows/fast-mlsirm-codeql.yml | 7 ++----- docs/product-technical-gap-baseline.md | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/fast-mlsirm-codeql.yml b/.github/workflows/fast-mlsirm-codeql.yml index 6829d2f9ab..18800abe22 100644 --- a/.github/workflows/fast-mlsirm-codeql.yml +++ b/.github/workflows/fast-mlsirm-codeql.yml @@ -13,7 +13,7 @@ permissions: jobs: analyze-actions: # Branch protection requires a deterministic Actions-language CodeQL check - # on PRs. Default setup remains the repository's code-scanning upload owner. + # on PRs. Default setup is disabled; this reusable workflow uploads SARIF. name: Analyze (actions) runs-on: group: CWL CI isolated @@ -30,14 +30,12 @@ jobs: languages: actions - uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd with: - upload: never output: codeql-results/actions - name: Keep CodeQL diagnostics local to the job run: test -s codeql-results/actions/actions.sarif analyze-python: - # Python remains a no-upload diagnostic for explicit operator runs; - # the current default setup is configured for Actions only. + # Python analysis and code-scanning upload require an explicit operator run. if: github.event_name == 'workflow_dispatch' name: Analyze (python) runs-on: @@ -55,7 +53,6 @@ jobs: languages: python - uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd with: - upload: never output: codeql-results/python - name: Keep CodeQL diagnostics local to the job run: test -s codeql-results/python/python.sarif diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c39bc9802e..a4ccac6fa0 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3168,7 +3168,7 @@ or whether default-setup landed on it (and possibly others) through an unrelated - **`fast-mlsirm`** — false positive. `.github/workflows/codeql.yml` runs two real jobs (`analyze-actions` on every PR, `analyze-python` gated to `workflow_dispatch` only), and **both** `analyze` steps carry `with: upload: never`, with comments stating *"Default setup remains the repository's code-scanning upload owner"* and *"Default setup already owns ordinary Python code-scanning uploads."* Confirmed via a live job log (run `33754939454`, job `100646992008`, `2026-09-04T00:45Z`): `upload: never` present in the action's resolved input dump, `Exported results to SARIF` followed by no upload call, job concluded `success`. Deliberately engineered the opposite way from `contextual-orchestrator`'s fix (default-setup keeps ownership, the local workflow stays silent) rather than the way `contextual-orchestrator` was fixed (local workflow keeps ownership, default-setup disabled) — both are valid resolutions of the same conflict; this repository already had one in place. No fix needed. - **`scopeweave`** — no live conflict, but two dangling artifacts worth a light cleanup. The workflow with real `init`/`analyze` steps (`.github/workflows/codeql.yml`) is `disabled_manually`, so it never runs and cannot collide with default-setup today. A second, unrelated workflow entry — "CodeQL Required," id `335384625`, `.github/workflows/codeql-required.yml` — is registered `state: "active"` in the Actions API, but the file itself no longer exists on the `develop` default branch (`404` on direct content fetch); GitHub retains the workflow-run registration for a file that has since been deleted, so this entry can never actually trigger. Net effect: default-setup is the sole current CodeQL coverage source for this repository, matching item-41's own "zero coverage from any source" criterion at whatever point `codeql.yml` was disabled — not a misclassification, just a repository whose local workflow went inactive after (or independent of) the rollout. Not fixed in this pass: re-enabling the disabled `codeql.yml` would immediately recreate `contextual-orchestrator`'s exact conflict, so any future re-enable of that workflow must add `upload: never` (matching `fast-mlsirm`'s pattern) or disable default-setup first, whichever this repository's owner intends as the coverage source of record. -**2026-10-11 diagnostic-storage repair (ContextualWisdomLab/.github#2607):** fast-mlsirm #2073 CodeQL job `114230230294` failed at `2026-10-10T15:36:55Z` because Actions artifact storage quota prevented its diagnostic SARIF upload. The central reusable workflow keeps SARIF local and requires a nonempty result after analysis. Default setup retains code-scanning upload ownership; live success for this revision is still pending. +**2026-10-11 update (ContextualWisdomLab/.github#2607):** the inventory above is historical. Live default setup now reports `state: not-configured`. fast-mlsirm #2073 CodeQL job `114230230294` failed at `2026-10-10T15:36:55Z` because Actions artifact quota blocked diagnostic SARIF retention. The reusable workflow now uploads SARIF directly to code-scanning, verifies a nonempty local SARIF, and avoids Actions diagnostic artifacts. Its current-head live result and base/head configuration continuity remain unverified. **The remaining 7 repositories** (`newsdom-api`, `keyverse`, `bandscope`, `mightyETL`, `litellm-patched-proxy`, `pg-erd-cloud`, `.github`) all returned `default-setup=not-configured` — no conflict is possible regardless of their local workflow's upload configuration. From 6723a2bd4127b05763cbe24dbf61e4951a058bc8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 11 Oct 2026 03:37:17 +0900 Subject: [PATCH 16/16] Allow owner-authorized public auto review routing --- .github/workflows/noema-review.yml | 2 ++ .../workflows/opencode-review-dispatch.yml | 4 +++ ...0-11-public-personal-auto-authorization.md | 22 ++++++++++++++++ scripts/ci/personal_review_route.py | 12 ++++++--- tests/test_personal_review_route.py | 25 +++++++++++++++++++ ...t_pr_review_autofix_nvidia_nim_contract.py | 2 +- 6 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 docs/adr/2026-10-11-public-personal-auto-authorization.md diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 023defa856..7439f7dc26 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -896,6 +896,7 @@ jobs: env: LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} PERSONAL_REVIEW_ZERO_COST_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false' }} + PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED: ${{ vars.PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED || 'false' }} PERSONAL_REVIEW_ZDR_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false' }} PRIVATE_TARGET: ${{ steps.target_visibility.outputs.require_zdr }} run: | @@ -906,6 +907,7 @@ jobs: fi python3 -m scripts.ci.personal_review_route \ --zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED" \ + --public-auto-authorized "$PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED" \ --zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED" \ --private-target "$PRIVATE_TARGET" echo 'selected=true' >>"$GITHUB_OUTPUT" diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 6c720c24b5..c56f546c47 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -2662,6 +2662,7 @@ jobs: env: LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} PERSONAL_REVIEW_ZERO_COST_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false' }} + PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED: ${{ vars.PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED || 'false' }} PERSONAL_REVIEW_ZDR_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false' }} PRIVATE_TARGET: ${{ needs.validate-pr-metadata.outputs.is_private }} run: | @@ -2672,6 +2673,7 @@ jobs: fi python3 -m scripts.ci.personal_review_route \ --zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED" \ + --public-auto-authorized "$PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED" \ --zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED" \ --private-target "$PRIVATE_TARGET" echo 'selected=true' >>"$GITHUB_OUTPUT" @@ -3777,6 +3779,7 @@ jobs: env: PERSONAL_ROUTE_SELECTED: ${{ steps.personal_route.outputs.selected }} PERSONAL_REVIEW_ZERO_COST_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false' }} + PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED: ${{ vars.PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED || 'false' }} PERSONAL_REVIEW_ZDR_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false' }} PRIVATE_TARGET: ${{ needs.validate-pr-metadata.outputs.is_private }} OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project @@ -4254,6 +4257,7 @@ jobs: if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then python3 -m scripts.ci.personal_review_route \ --zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED" \ + --public-auto-authorized "$PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED" \ --zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED" \ --private-target "$PRIVATE_TARGET" \ --opencode-config "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" diff --git a/docs/adr/2026-10-11-public-personal-auto-authorization.md b/docs/adr/2026-10-11-public-personal-auto-authorization.md new file mode 100644 index 0000000000..96b4e61d1c --- /dev/null +++ b/docs/adr/2026-10-11-public-personal-auto-authorization.md @@ -0,0 +1,22 @@ +# Owner authorization for public personal auto review + +## Status + +Proposed in ContextualWisdomLab/.github#2607; the maintainer's requested endpoint and model are explicit session instructions. + +## Context and decision drivers + +The maintainer requested `https://litellm.poinnetworks.net` with model `auto`. Its scoped inference key is configured, but supplier cost metadata is absent. Authentication does not establish zero cost or retention. The existing admission helper consequently blocks the requested public route. + +## Considered options + +- Require zero-cost evidence for every personal route. +- Admit the explicitly authorized public route while retaining private supplier gates. + +## Decision outcome + +Use the second option. A maintainer-controlled `PERSONAL_REVIEW_PUBLIC_AUTO_AUTHORIZED=true` may admit only a definitely public target. Its default is false. It is routing authorization, not PR approval or supplier attestation. Private and unknown targets cannot use this exception; private routing still requires zero-cost and zero-retention attestations. The fixed endpoint, literal wire model `auto`, read-only review permissions, source-bound verdict validation, independent reviewer identity and required checks remain unchanged. + +## Consequences + +Public review can use the requested route without a false cost declaration. Supplier cost remains unknown. Private disclosure and protected merge are still fail-closed; a configured key alone proves neither inference nor approval. diff --git a/scripts/ci/personal_review_route.py b/scripts/ci/personal_review_route.py index b417285813..01c53bc505 100644 --- a/scripts/ci/personal_review_route.py +++ b/scripts/ci/personal_review_route.py @@ -9,9 +9,11 @@ MODEL = 'personal-litellm/auto' -def require_admission(*, zero_cost: bool, zero_retention: bool, private_target: bool) -> None: - """Reject missing supplier evidence; existing key possession is not policy.""" - if zero_cost is not True: +def require_admission(*, zero_cost: bool, zero_retention: bool, private_target: bool, + public_auto_authorized: bool = False) -> None: + """Admit owner-authorized public routing; private supplier gates stay intact.""" + authorized_public = private_target is False and public_auto_authorized is True + if zero_cost is not True and not authorized_public: raise RuntimeError('STOP: personal auto zero-cost supplier is not attested') if private_target and zero_retention is not True: raise RuntimeError('STOP: private personal auto zero-retention supplier is not attested') @@ -39,11 +41,13 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument('--zero-cost', choices=('true', 'false'), required=True) parser.add_argument('--zero-retention', choices=('true', 'false'), required=True) parser.add_argument('--private-target', choices=('true', 'false'), required=True) + parser.add_argument('--public-auto-authorized', choices=('true', 'false'), default='false') parser.add_argument('--opencode-config', type=Path) args = parser.parse_args(argv) require_admission(zero_cost=args.zero_cost == 'true', zero_retention=args.zero_retention == 'true', - private_target=args.private_target == 'true') + private_target=args.private_target == 'true', + public_auto_authorized=args.public_auto_authorized == 'true') if args.opencode_config is not None: configure_opencode(args.opencode_config) return 0 diff --git a/tests/test_personal_review_route.py b/tests/test_personal_review_route.py index 4317810d91..eaca1e5e54 100644 --- a/tests/test_personal_review_route.py +++ b/tests/test_personal_review_route.py @@ -108,3 +108,28 @@ def open(self, request): assert url == 'https://litellm.poinnetworks.net/v1/chat/completions' assert body['model'] == 'auto' and 'temperature' not in body assert body['response_format']['type'] == 'json_schema' + + +def test_authorized_public_route_does_not_assert_supplier_cost(): + module().require_admission(zero_cost=False, zero_retention=False, + private_target=False, public_auto_authorized=True) + for private in (True, None): + with pytest.raises(RuntimeError, match='zero-cost supplier'): + module().require_admission(zero_cost=False, zero_retention=True, + private_target=private, public_auto_authorized=True) + for authorization in (False, None, 'true', 1): + with pytest.raises(RuntimeError, match='zero-cost supplier'): + module().require_admission(zero_cost=False, zero_retention=True, + private_target=False, public_auto_authorized=authorization) + + +def test_public_authorization_cli_cannot_release_private_configuration(tmp_path): + path = tmp_path / 'config.json' + original = '{"permission":{"edit":"deny"}}' + path.write_text(original) + base = ['--zero-cost', 'false', '--zero-retention', 'false', + '--public-auto-authorized', 'true'] + assert module().main(base + ['--private-target', 'false']) == 0 + with pytest.raises(RuntimeError, match='zero-cost supplier'): + module().main(base + ['--private-target', 'true', '--opencode-config', str(path)]) + assert path.read_text() == original diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 726fb1fc59..a35bc44c45 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 = "6c720c24b51d38be27138c71c77f5c3994c8352b" +REVIEW_DISPATCH_BLOB_SHA = "c56f546c4767407d39cc7cad1b51e1440b1545c6" def _workflow_text(path: Path) -> str: