From 65265c23b1279fa818b0fd17bde97facf2495718 Mon Sep 17 00:00:00 2001 From: Alex Mikheev Date: Fri, 11 Sep 2026 23:48:47 +0100 Subject: [PATCH 1/3] ci: V-model pipeline for native-ci and GitHub CI, kept in sync (Refs #254) Same change as 799f6e3 on the Gitea lineage, applied on top of the GitHub main lineage so the public CI matrix can run it. See docs/ci-gap-analysis.md. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WjGks4DGYdDw3QLgu32S96 --- .config/nextest.toml | 28 +++++++++ .gitea/workflows/native-ci.yml | 102 +++++++++++++++++++++---------- .github/workflows/ci.yml | 99 +++++++++++++++++++++++++++++- deny.toml | 101 ++++++++++++++++++++++++++++++ docs/ci-gap-analysis.md | 108 +++++++++++++++++++++++++++++++++ rust-toolchain.toml | 8 +++ 6 files changed, 412 insertions(+), 34 deletions(-) create mode 100644 .config/nextest.toml create mode 100644 deny.toml create mode 100644 docs/ci-gap-analysis.md create mode 100644 rust-toolchain.toml diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..eb82a9d --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,28 @@ +# cargo-nextest configuration (Refs #254). +# +# `default` is what developers get locally. `ci` is selected in the workflows +# with `--profile ci`: no fail-fast so one red target does not hide the rest, +# JUnit output for the runner to archive, and bounded retries only for tests +# that are known to depend on a live terraphim_server process. + +[profile.default] +retries = 0 +fail-fast = true +slow-timeout = { period = "60s", terminate-after = 3 } + +[profile.ci] +retries = 0 +fail-fast = false +slow-timeout = { period = "60s", terminate-after = 3 } + +[profile.ci.junit] +path = "junit.xml" + +# The integration binaries that shell out to a real terraphim_server +# (TERRAPHIM_SERVER_BIN) are the only ones allowed a retry and a longer +# slow-timeout, and only in CI: they wait on a live HTTP service. Everything +# else must be deterministic; a flaky test is a bug, not a retry candidate. +[[profile.ci.overrides]] +filter = 'binary(/^(server_mode_tests|cross_mode_consistency_test|integration_tests|kg_ranking_integration_test)$/)' +retries = 1 +slow-timeout = { period = "120s", terminate-after = 3 } diff --git a/.gitea/workflows/native-ci.yml b/.gitea/workflows/native-ci.yml index d975ddf..805269c 100644 --- a/.gitea/workflows/native-ci.yml +++ b/.gitea/workflows/native-ci.yml @@ -1,25 +1,45 @@ name: native-ci +# V-model gate order (Refs #254): fast checks -> tests -> coverage/security -> +# nightly UB gates. Kept in sync with .github/workflows/ci.yml; see +# docs/ci-gap-analysis.md "Sync rule". Every step's first token must be on the +# terraphim-gitea-runner allowlist (cargo, rustup, bash, test, ...); curl and +# python are denied, so tools are provisioned with `cargo install`. on: push: workflow_dispatch: jobs: - build: + # Stage 1: fast checks (fmt, clippy, deny). Fails in seconds, not minutes. + check: runs-on: terraphim-native steps: + - run: rustup show active-toolchain - run: cargo fmt --all -- --check - run: cargo clippy --workspace --all-targets -- -D warnings + # #2171: enrichment feature is not covered by default-features clippy. + - run: cargo clippy -p terraphim_sessions --features enrichment -- -D warnings + # Supply chain: advisories, licences, bans, sources per deny.toml. + # cargo-deny is present on runner-5 only, so install idempotently. + - run: cargo install cargo-deny --locked --version 0.20.2 + - run: cargo deny check + # Stage 2: build + full test gate against a real terraphim_server. + test: + needs: check + runs-on: terraphim-native + steps: + # #106: the terraphim_update signed-archive tests shell out to the + # `zipsign` binary on the host. It is installed at /usr/local/bin/zipsign + # on bigbox (see "Host Tooling" in gitea-infrastructure HANDOVER.md); + # runners carry their own CARGO_HOME and do not all have ~/.cargo/bin on + # PATH, so a user-local install is invisible to them. Fail fast with a + # pointer to the fix rather than 21 failing test targets. + - name: Check host tooling (zipsign) + # Note: the terraphim-gitea-runner command policy inspects the + # literal first token; `if`/`then`/`fi` shell keywords get rejected. + # Use `test` (the only conditional primitive on the allowlist) and + # `||` chaining instead. Refs #106. + run: | + test -x /usr/local/bin/zipsign && /usr/local/bin/zipsign --version || { echo "::error::zipsign not found on PATH. Install on the runner host: sudo install -m 0755 ~/.cargo/bin/zipsign /usr/local/bin/zipsign (see gitea-infrastructure HANDOVER.md, 'Host Tooling'). Refs #106"; exit 1; } - run: cargo build --workspace - # #84: broaden the workspace gate so integration tests, binary - # smoke tests, and example doctests participate in the gate. The - # narrower `--lib` gate silently skipped crates/terraphim_mcp_server - # tests/test_tools_list.rs and tests/test_all_mcp_tools.rs, which - # exercised real stdio/JSON-RPC round-trips against the - # terraphim_mcp_server binary. `--tests --bins --examples` adds - # those targets; `--lib` is kept so the existing crate-internal - # coverage is still exercised. `--no-fail-fast` makes all targets - # run even when one fails, so a single broken test does not hide - # the rest of the failures behind an early abort. - - run: cargo test --workspace --tests --bins --examples --lib --no-fail-fast # #113: build terraphim_server from terraphim-ai so the # server-binary-dependent integration tests have a real binary. # terraphim_server is not a workspace member here -- it lives in @@ -41,22 +61,42 @@ jobs: # requirements (1.20.2) match both 1.20.2 and 1.21.0 in the registry # and cargo aborts with "patch resolved to more than one candidate". - run: cargo install --locked --git https://git.terraphim.cloud/terraphim/terraphim-ai --tag v1.21.3 --root /tmp/terraphim_server_install --config 'registries.terraphim.index="sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/"' --config 'registry.global-credential-providers=["cargo:token"]' --bin terraphim_server terraphim_server - # #113: run the integration tests that require a real - # terraphim_server binary. ensure_server_binary() (in - # cross_mode_consistency_test.rs / kg_ranking_integration_test.rs) - # and server_binary_path() (in integration_tests.rs) both resolve - # TERRAPHIM_SERVER_BIN first, so pointing the env var at the - # install root is enough. - - run: TERRAPHIM_SERVER_BIN=/tmp/terraphim_server_install/bin/terraphim_server cargo test -p terraphim_agent --test cross_mode_consistency_test -- --nocapture - - run: TERRAPHIM_SERVER_BIN=/tmp/terraphim_server_install/bin/terraphim_server cargo test -p terraphim_agent --test integration_tests -- --nocapture - - run: TERRAPHIM_SERVER_BIN=/tmp/terraphim_server_install/bin/terraphim_server cargo test -p terraphim_agent --test kg_ranking_integration_test -- --nocapture - # #2171: enrichment feature clippy + test invocations. - - run: cargo clippy -p terraphim_sessions --features enrichment -- -D warnings - - run: cargo test -p terraphim_sessions --features enrichment --lib --no-fail-fast - # #95: isolated packaged install-graph regression. - - run: cargo test -p terraphim_agent --test packaged_install_graph_regression -- --nocapture - # #118: repo guards -- duplicate-crate detection and the publish gate's own - # tests. Rust tests, not shell steps: the runner allowlist rejects any - # program that is not cargo ("policy rejected command: ... not on the - # allowlist"), which is what took CI down from #112 until now. - - run: cargo test -p terraphim_agent --test ci_guards -- --nocapture + # nextest: one binary per target, parallel scheduling, JUnit output + # (.config/nextest.toml profile ci). --all-targets keeps the + # terraphim-agents#91 coverage: binaries, examples and tests/*.rs. + - run: cargo install cargo-nextest --locked + - run: TERRAPHIM_SERVER_BIN=/tmp/terraphim_server_install/bin/terraphim_server cargo nextest run --workspace --all-targets --profile ci + # nextest does not run doctests. + - run: cargo test --workspace --doc --no-fail-fast + # #113: focused re-runs of the server-backed integration tests (also in + # the run above; kept for fast failure attribution). + - run: TERRAPHIM_SERVER_BIN=/tmp/terraphim_server_install/bin/terraphim_server cargo nextest run -p terraphim_agent --test cross_mode_consistency_test --test integration_tests --test kg_ranking_integration_test --profile ci + # #2171 / terraphim-clients#150: feature lanes invisible to default features. + - run: cargo nextest run -p terraphim_sessions --features enrichment --lib --profile ci + - run: cargo nextest run -p terraphim_sessions --all-features --profile ci + # #95 / #118: focused regression and repo guards (Rust tests, not shell + # steps: the runner allowlist rejects non-cargo programs). + - run: cargo nextest run -p terraphim_agent --test packaged_install_graph_regression --test ci_guards --profile ci + # Stage 3: coverage gate. Threshold only ever goes up (docs/ci-gap-analysis.md). + coverage: + needs: check + runs-on: terraphim-native + steps: + - run: rustup component add llvm-tools + - run: cargo install cargo-llvm-cov --locked + - run: cargo llvm-cov --workspace --lib --lcov --output-path lcov.info + - run: cargo llvm-cov report --fail-under-lines 65 + # Stage 4: UB gates on nightly (from the #252 UB_RUNBOOK). Miri covers the + # pure-computation crates only; tokio/reqwest paths are unsupported by Miri. + # terraphim_hooks' discovery tests spawn a process (posix_spawn), which Miri + # cannot emulate, and validation::tests::test_validate_latency runs 1000 + # timed iterations that are meaningless (and very slow) under Miri, hence + # --skip discovery --skip latency. + ub-gates: + needs: check + runs-on: terraphim-native + steps: + - run: rustup toolchain install nightly --profile minimal -c miri -c rust-src + - run: cargo +nightly miri setup + - run: MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test -p terraphim_negative_contribution -p terraphim_command_runtime -p terraphim_hooks --lib -- --skip discovery --skip latency + - run: MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-tree-borrows" cargo +nightly miri test -p terraphim_negative_contribution -p terraphim_command_runtime -p terraphim_hooks --lib -- --skip discovery --skip latency diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a2a12a..164c83f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,10 @@ name: CI +# Public mirror of .gitea/workflows/native-ci.yml (Refs #254). Same gate +# order and the same cargo invocations; differences are limited to +# provisioning (GitHub actions instead of cargo install on a self-hosted +# runner), the multi-platform matrix, and the absence of the terraphim_server +# backed integration lane, which needs the private registry. See +# docs/ci-gap-analysis.md "Sync rule". on: push: branches: [main] @@ -9,17 +15,104 @@ on: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 + # The 1.21.x terraphim family resolves from the private Gitea registry + # (see [patch.crates-io] in Cargo.toml). Same secret release-binaries uses. + CARGO_REGISTRIES_TERRAPHIM_TOKEN: ${{ secrets.CARGO_REGISTRIES_TERRAPHIM_TOKEN }} jobs: - build: + # Stage 1: fast checks + check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@master with: + toolchain: 1.97.1 components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - run: cargo fmt --all -- --check - run: cargo clippy --workspace --all-targets -- -D warnings + - run: cargo clippy -p terraphim_sessions --features enrichment -- -D warnings + - uses: taiki-e/install-action@cargo-deny + - run: cargo deny check + + # Stage 2: tests on every release platform + test: + needs: check + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.97.1 + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@nextest - run: cargo build --workspace - - run: cargo test --workspace --lib --no-fail-fast + # Lib and doc tests on all platforms. Integration tests that need a + # terraphim_server binary run only on native-ci. + - run: cargo nextest run --workspace --lib --profile ci + - run: cargo test --workspace --doc --no-fail-fast + # #2171: enrichment-feature test invocation. + - run: cargo nextest run -p terraphim_sessions --features enrichment --lib --profile ci + # #4325: zero-chunk smoke for terraphim_grep default features. + - run: cargo nextest run -p terraphim_grep --test default_feature_smoke --profile ci + # #95: isolated packaged install-graph regression. + - run: cargo nextest run -p terraphim_agent --test packaged_install_graph_regression --profile ci + + # Stage 3: coverage gate (Linux only; threshold only ever increases) + coverage: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.97.1 + components: llvm-tools + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@cargo-llvm-cov + - run: cargo llvm-cov --workspace --lib --lcov --output-path lcov.info + - run: cargo llvm-cov report --fail-under-lines 65 + - uses: actions/upload-artifact@v4 + with: + name: lcov + path: lcov.info + + # Stage 4: UB gates on nightly (from the #252 UB_RUNBOOK) + ub-gates: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + with: + components: miri, rust-src + - uses: Swatinem/rust-cache@v2 + - run: cargo +nightly miri setup + - run: MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test -p terraphim_negative_contribution -p terraphim_command_runtime -p terraphim_hooks --lib -- --skip discovery --skip latency + - run: MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-tree-borrows" cargo +nightly miri test -p terraphim_negative_contribution -p terraphim_command_runtime -p terraphim_hooks --lib -- --skip discovery --skip latency + + # Stage 5: benchmark regression, informational on PRs (baselines from #253) + benchmarks: + if: github.event_name == 'pull_request' + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.97.1 + - uses: Swatinem/rust-cache@v2 + - run: cargo bench -p terraphim_grep --features code-search --bench hybrid_search -- --output-format bencher | tee bench-output.txt + - uses: benchmark-action/github-action-benchmark@v1 + with: + tool: cargo + output-file-path: bench-output.txt + alert-threshold: "120%" + comment-on-alert: true + fail-on-alert: false + auto-push: false diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..14e6561 --- /dev/null +++ b/deny.toml @@ -0,0 +1,101 @@ +# cargo-deny configuration (Refs #254). +# +# Policy: advisories and yanked crates block the pipeline; unmaintained and +# notice-level advisories warn. Every `ignore` entry below carries the reason +# and the issue that owns its removal. Ignores are temporary by construction: +# remove the entry as soon as the named fix lands. + +[graph] +all-features = true +targets = [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", +] + +[advisories] +version = 2 +db-urls = ["https://github.com/rustsec/advisory-db"] +# TEMPORARY: warn until the Cargo.lock bump lands (spin 0.9.8 is yanked and is +# fixed by `cargo update -p spin`). Flip back to "deny" in that commit. +yanked = "warn" +ignore = [ + # --- Lockfile bumps pending (semver-compatible, `cargo update -p `). + # Held back until the UB audit #252 finishes its Phase 3 dynamic runs, so + # Miri/TSan results are attributed to one dependency set. Remove all six + # entries in the same commit as the Cargo.lock bump. + { id = "RUSTSEC-2026-0204", reason = "crossbeam-epoch 0.9.18 -> 0.9.21 via cargo update; blocked on #252 Phase 3" }, + { id = "RUSTSEC-2026-0258", reason = "h2 0.4.15 -> 0.4.19 via cargo update; blocked on #252 Phase 3" }, + { id = "RUSTSEC-2026-0190", reason = "anyhow 1.0.102 -> 1.0.104 via cargo update; blocked on #252 Phase 3" }, + # The same Cargo.lock bump also clears the yanked spin 0.9.8 (cargo update -p spin). + # Unsound-class advisories cargo-audit reports for event-listener, lru, memmap2 and + # git2 (RUSTSEC-2026-0221/0253/0186/0183/0184) are informational in cargo-deny and + # are not encountered by this config; the first three are fixed by the same bump, + # git2 needs fff-search to move to a patched release (child issue of #254). + # --- quick-xml: two lockfile copies, both below the fixed 0.41. + { id = "RUSTSEC-2026-0195", reason = "quick-xml 0.37 (self_update via terraphim_update 1.20.2 registry) and 0.38 (opendal 0.54); fix is upstream in self_update and opendal. Child issue of #254" }, + { id = "RUSTSEC-2026-0194", reason = "same crate and chain as RUSTSEC-2026-0195" }, + # --- Requires an upstream or major-version change. + { id = "RUSTSEC-2026-0189", reason = "rmcp 0.9.1 -> >=1.4.0 is a major upgrade of terraphim_mcp_server's transport; DNS-rebinding affects the streamable HTTP server only. Tracked as a child of #254" }, + # --- Unmaintained (warn-level; listed so the reason is recorded). + { id = "RUSTSEC-2025-0141", reason = "bincode 1.x via transitive deps; no maintained drop-in" }, + { id = "RUSTSEC-2025-0119", reason = "number_prefix via indicatif" }, + { id = "RUSTSEC-2024-0436", reason = "paste proc-macro; compile-time only" }, + { id = "RUSTSEC-2024-0370", reason = "proc-macro-error; compile-time only" }, +] + +[licenses] +version = 2 +confidence-threshold = 0.9 +allow = [ + "MIT", + "MIT-0", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "0BSD", + "Unlicense", + "CC0-1.0", + "BSL-1.0", + "Unicode-3.0", + "CDLA-Permissive-2.0", + "MPL-2.0", + "bzip2-1.0.6", +] +# LGPL appears only as one option of a tri-licensed crate (r-efi: MIT OR +# Apache-2.0 OR LGPL-2.1-or-later); cargo-deny picks an allowed option, so +# no exception is needed. + +[[licenses.exceptions]] +# html2md is GPL-3.0+ and reaches terraphim_agent and terraphim-cli +# (both Apache-2.0) through terraphim_middleware (terraphim-ai registry). +# This is an upstream licence-compatibility problem, not a local one: the +# fix is for terraphim_middleware to replace html2md or gate it behind a +# non-default feature. Recorded here so the gate stays green while the +# upstream issue is open; remove when terraphim_middleware drops html2md. +allow = ["GPL-3.0-or-later", "GPL-3.0"] +name = "html2md" + +[bans] +multiple-versions = "warn" +wildcards = "deny" +highlight = "all" +deny = [ + { name = "openssl-sys", reason = "workspace policy is rustls everywhere; native TLS breaks the static release-binaries matrix" }, +] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = [ + "https://github.com/rust-lang/crates.io-index", + "sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/", +] +allow-git = [ + "https://github.com/rustls/webpki", +] diff --git a/docs/ci-gap-analysis.md b/docs/ci-gap-analysis.md new file mode 100644 index 0000000..823a2ce --- /dev/null +++ b/docs/ci-gap-analysis.md @@ -0,0 +1,108 @@ +# CI/CD gap analysis and staged pipeline + +Refs terraphim/terraphim-clients#254. Written 2026-09-11 against commit 1120180. + +## Pipelines in play + +| Pipeline | File | Runner | Role | +|---|---|---|---| +| native-ci | `.gitea/workflows/native-ci.yml` | `terraphim-native` (bigbox, 24 cores) | Source of truth. Full workspace gate including the terraphim_server-backed integration lane. | +| CI | `.github/workflows/ci.yml` | GitHub hosted | Public mirror for the crates.io / GitHub build of terraphim-agent. Multi-platform matrix. | +| release-binaries, publish-crates, publish-registry, r2-manifest-health | `.github/workflows/*`, `.gitea/workflows/publish-registry.yml` | mixed | Release and distribution. Out of scope here except where gates feed them. | + +## Gates before and after + +| V-model stage | Gate | Before | After (this change) | +|---|---|---|---| +| Implementation | fmt, clippy `-D warnings` | present, in one monolithic job | own `check` job, runs first, fails in seconds | +| Implementation | pinned toolchain | none | `rust-toolchain.toml` = 1.97.1; GitHub pins the same version | +| Verification | unit and integration tests | `cargo test --all-targets` | `cargo nextest run --all-targets --profile ci` with JUnit output; doctests via `cargo test --doc` | +| Verification | coverage threshold | none | `cargo llvm-cov --fail-under-lines N` (N set from the measured baseline, see below) | +| Verification | supply chain | none in CI; `cargo deny` failed locally | `deny.toml` with every advisory triaged; `cargo deny check` in `check` | +| Verification | Miri on unsafe or pure crates | none | `ub-gates` job on nightly, default and tree-borrows axes, crates chosen by the #252 runbook | +| Verification | benchmark regression | none | GitHub `benchmarks` job on PRs, informational, using the #253 baselines | +| Validation | multi-platform | Linux only | GitHub `test` matrix: ubuntu, macos, windows | +| Validation | release artefacts | mature | unchanged | + +## Runner constraints that shaped the design + +The terraphim-gitea-runner enforces a command policy on the literal first token of each step. Allowed: `cargo`, `rustup`, `bash`, `sh`, `test`, `git`, `make`, `rch` and a few coreutils. Denied: `curl`, `wget`, `docker`, `python`. Consequences: + +- Tools are provisioned with `cargo install --locked` inside the job. Runners have separate `CARGO_HOME`s and uneven tool sets (nextest and miri on some, cargo-deny on runner-5 only, cargo-llvm-cov nowhere), so provisioning is idempotent and repeated per job. +- `cargo build`, `check`, `clippy` and `doc` are routed to the rch compile farm by policy; `cargo nextest`, `cargo llvm-cov`, `cargo deny` and `cargo +nightly miri` run on the host. +- Repository scripts must be invoked as `bash ./scripts/x.sh`; `./scripts/x.sh` is rejected. +- No `uses:` actions on the native runner; checkout is implicit. +- Gitea dispatches one job per task as a "SingleWorkflow" payload, so `needs:` ordering and job fan-out are handled by Gitea itself; the runner only ever sees a single job. The four-job split therefore works on native-ci and the `check` job really does fail fast. +- The runner reports a commit status per job as `native-ci / (push)`. Branch protection on `main` currently lists `native-ci / build (push)` (status checks disabled at the time of writing). Because this change renames `build` to `check` and `test`, the protection rule must be updated when status checks are re-enabled. + +## Advisory triage (deny.toml) + +| Advisory | Crate and chain | Action | +|---|---|---| +| RUSTSEC-2026-0204 crossbeam-epoch | fff-search, ignore | `cargo update -p crossbeam-epoch` (0.9.21) | +| RUSTSEC-2026-0258 h2 | hyper, axum, rmcp, reqwest | `cargo update -p h2` (0.4.19) | +| RUSTSEC-2026-0190 anyhow (unsound downcast_mut) | opendal, terraphim_config | `cargo update -p anyhow` (1.0.104) | +| RUSTSEC-2026-0221 event-listener (!Send crossing) | sqlx via opendal | `cargo update -p event-listener` (5.4.2) | +| RUSTSEC-2026-0253 lru (UAF on panic in pop) | transitive | `cargo update -p lru` (0.18.4) | +| RUSTSEC-2026-0186 memmap2 | transitive | `cargo update -p memmap2` (0.9.11) | +| yanked spin 0.9.8 | transitive | `cargo update -p spin` | +| RUSTSEC-2026-0189 rmcp DNS rebinding | terraphim_mcp_server direct dep, 0.9.1 | major upgrade to rmcp 1.4; child issue | +| RUSTSEC-2026-0183 / 0184 git2 UB | fff-search | needs fff-search to adopt a patched git2; child issue | +| RUSTSEC-2026-0194 / 0195 quick-xml | self_update (via terraphim_update) and opendal 0.54 | upstream; child issue | +| RUSTSEC-2023-0071 rsa Marvin, quinn-proto 0.11.14 | in Cargo.lock but not in the resolved all-features graph | not reachable; cargo-audit reports them from the lockfile only | +| five unmaintained notices | bincode, instant, number_prefix, paste, proc-macro-error | recorded, warn level | + +The seven lockfile bumps are one commit. They are held until the UB audit (#252) finishes its Phase 3 dynamic runs so Miri and TSan results are attributed to a single dependency set. Until then `deny.toml` carries dated ignore entries and `yanked = "warn"`; that commit removes the entries and restores `yanked = "deny"`. + +### Licence finding + +`html2md` (GPL-3.0+) reaches `terraphim_agent` and `terraphim-cli` (Apache-2.0) through `terraphim_middleware` from the terraphim registry. This is an upstream licence-compatibility problem in terraphim-ai; `deny.toml` carries a named exception so the gate is green, and the exception must go when terraphim_middleware drops or feature-gates html2md. + +## Why GitHub CI has been red since 2026-09-01 + +Every run fails within twenty seconds at `cargo fmt --check`. Locally the tree is rustfmt-clean on 1.97.1. The GitHub workflow used `dtolnay/rust-toolchain@stable`, which resolves to the newest stable and its newer rustfmt, whose output differs from 1.97.1 on this tree. Pinning the toolchain in the workflow (and `rust-toolchain.toml`) is the fix; no source formatting change is needed. + +## A test that only fails under workspace feature unification + +`terraphim-session-analyzer::connectors::codex::tests::test_parse_response_item` passes when run with `-p terraphim-session-analyzer` and fails under `cargo test --workspace --lib` (the GitHub CI command) because feature unification across the workspace changes the `ResponseItem` payload parsing. This is a pre-existing red, independent of this change; it is tracked in its own issue and the coverage baseline was measured with `--ignore-run-fail`. + +## Coverage threshold + +The threshold in both workflows is set from the first measured baseline, rounded down to the nearest 5. It only ever increases. + +Baseline measured 2026-09-11 on commit 1120180, macOS aarch64, `cargo llvm-cov --workspace --lib --ignore-run-fail`: + +| Metric | Covered | Total | Percent | +|---|---|---|---| +| Lines | 23753 | 35183 | 67.51 | +| Functions | 2425 | 3615 | 67.08 | +| Regions | 37948 | 54538 | 69.58 | + +Gate: `--fail-under-lines 65`. Per-crate targets from the skill's table (80 percent for library crates, 70 for binaries, 90 for any module that carries `unsafe`) are the direction of travel, not the gate today. + +## Miri crate selection + +Miri cannot execute tokio, mio, reqwest or process spawning. The `ub-gates` job therefore runs the pure-computation crates only: terraphim_negative_contribution, terraphim_command_runtime and terraphim_hooks. The hooks `discovery` tests spawn a subprocess (`posix_spawnattr_init` is unsupported by Miri) and are skipped with `-- --skip discovery`, and the 1000-iteration latency test in the same crate is skipped with `--skip latency` because wall-clock assertions are meaningless under Miri. The #252 runbook widens this list as Phase 3 establishes which other test modules are Miri-clean (session-analyzer parsing and sessions redaction are the next candidates). + +## Sync rule + +`native-ci.yml` is the source of truth. Any change to a `cargo` invocation in a gate is made there first and mirrored into `ci.yml` in the same commit. Allowed divergence between the two: + +1. Provisioning: `cargo install` on the native runner versus `taiki-e/install-action` on GitHub. +2. The terraphim_server-backed integration lane exists only on native-ci (private registry). +3. The multi-platform matrix and the benchmark job exist only on GitHub. + +A PR that changes one file and not the other is rejected in review unless the diff falls into one of those three categories. + +## Staged rollout + +1. Land `rust-toolchain.toml`, `deny.toml`, `.config/nextest.toml` and the restructured workflows (this branch). Coverage threshold at the measured floor. +2. After #252 Phase 3: the lockfile bump commit; remove the six ignores; `yanked = "deny"`. +3. After #252 Phase 12: extend the Miri crate list from the runbook; add the TSan lane if the runner can build-std. +4. After #251 lands feature flags: add `--features otel,prometheus` to the clippy and test lanes. +5. After #253 commits baselines: point the benchmark job at `docs/perf/` baselines and raise the alert threshold from informational to blocking on main. + +## Open items + +- `cargo-llvm-cov` on the native runner needs `rustup component add llvm-tools` for the pinned 1.97.1 toolchain; the job does this each run. +- The Windows leg of the GitHub matrix has never run this workspace's tests; expect path or `zipsign` related failures on first run and gate them with `if: runner.os != 'Windows'` only with a recorded reason. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..7d40638 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,8 @@ +# Pinned toolchain for reproducible CI and local builds (Refs #254). +# Matches the active stable on the terraphim-native runner host and on +# developer machines as of 2026-09-11. Bump deliberately, in its own commit, +# and re-run the full native-ci gate before merging. +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy"] +profile = "minimal" From f8ccd981253dc1cd53190faad3ca22319ae19795 Mon Sep 17 00:00:00 2001 From: Alex Mikheev Date: Fri, 11 Sep 2026 23:51:00 +0100 Subject: [PATCH 2/3] style: rustfmt the GitHub lineage under the pinned 1.97.1 toolchain (Refs #254) Whitespace-only. These files were committed on the GitHub main lineage without rustfmt; the Gitea lineage is already clean. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WjGks4DGYdDw3QLgu32S96 --- .../src/analyzer.rs | 4 +- .../terraphim-session-analyzer/src/models.rs | 2 +- .../terraphim-session-analyzer/src/parser.rs | 8 +-- .../src/patterns/matcher.rs | 2 +- crates/terraphim_agent/src/client.rs | 3 - .../terraphim_agent/src/learnings/capture.rs | 56 ++++++++----------- crates/terraphim_agent/src/main.rs | 22 ++------ .../tests/kg_ranking_integration_test.rs | 1 - .../tests/test_all_mcp_tools.rs | 49 +++++++++++----- .../tests/test_tools_list.rs | 12 ++-- 10 files changed, 79 insertions(+), 80 deletions(-) diff --git a/crates/terraphim-session-analyzer/src/analyzer.rs b/crates/terraphim-session-analyzer/src/analyzer.rs index 242e43a..be91052 100644 --- a/crates/terraphim-session-analyzer/src/analyzer.rs +++ b/crates/terraphim-session-analyzer/src/analyzer.rs @@ -55,7 +55,7 @@ impl Analyzer { /// /// Public API consumed only by cross-binary integration tests. /// Consumers: `tests/integration_tests.rs`. - #[must_use] + #[must_use] /// Public API consumed only by `tests/integration_tests.rs` (cross-binary integration test). The `tsa` binary does not call this method. #[allow(dead_code)] pub fn with_config(mut self, config: AnalyzerConfig) -> Self { @@ -765,7 +765,7 @@ impl Analyzer { /// Public API consumed only by cross-binary integration tests /// (in-file unit tests in this module also exercise it directly). /// Consumers: `tests/integration_tests.rs` and lib unit tests in this file. - #[must_use] + #[must_use] /// Public API consumed only by lib unit tests in this file and `tests/integration_tests.rs` (cross-binary integration test). The `tsa` binary does not call this method. #[allow(dead_code)] pub fn detect_tool_chains( diff --git a/crates/terraphim-session-analyzer/src/models.rs b/crates/terraphim-session-analyzer/src/models.rs index 7f76c23..76714d3 100644 --- a/crates/terraphim-session-analyzer/src/models.rs +++ b/crates/terraphim-session-analyzer/src/models.rs @@ -134,7 +134,7 @@ impl ToolCategory { /// Public API consumed only by integration tests and downstream callers. /// The `tsa` binary does not call this method, hence the conditional allow. /// Consumers: `tests/integration_tests.rs` (cross-binary integration test). - #[must_use] + #[must_use] /// Public API consumed only by `tests/integration_tests.rs` (cross-binary integration test). The `tsa` binary does not call this method. #[allow(dead_code)] pub fn from_string(s: &str) -> Self { diff --git a/crates/terraphim-session-analyzer/src/parser.rs b/crates/terraphim-session-analyzer/src/parser.rs index c34f93b..2941a80 100644 --- a/crates/terraphim-session-analyzer/src/parser.rs +++ b/crates/terraphim-session-analyzer/src/parser.rs @@ -341,7 +341,7 @@ impl SessionParser { /// /// Public API consumed only by cross-binary integration tests. /// Consumers: `tests/integration_tests.rs`. - #[must_use] + #[must_use] /// Public API consumed only by `tests/integration_tests.rs` (cross-binary integration test). The `tsa` binary does not call this method. #[allow(dead_code)] pub fn entry_count(&self) -> usize { @@ -358,7 +358,7 @@ impl SessionParser { /// /// Public API consumed only by cross-binary integration tests. /// Consumers: `tests/integration_tests.rs`. - #[must_use] + #[must_use] /// Public API consumed only by `tests/integration_tests.rs` (cross-binary integration test). The `tsa` binary does not call this method. #[allow(dead_code)] pub fn entries_in_window( @@ -385,7 +385,7 @@ impl SessionParser { /// /// Public API consumed only by cross-binary integration tests. /// Consumers: `tests/integration_tests.rs`. - #[must_use] + #[must_use] /// Public API consumed only by `tests/integration_tests.rs` (cross-binary integration test). The `tsa` binary does not call this method. #[allow(dead_code)] pub fn get_agent_types(&self) -> Vec { @@ -404,7 +404,7 @@ impl SessionParser { /// /// Public API consumed only by cross-binary integration tests. /// Consumers: `tests/integration_tests.rs`. - #[must_use] + #[must_use] /// Public API consumed only by `tests/integration_tests.rs` (cross-binary integration test). The `tsa` binary does not call this method. #[allow(dead_code)] pub fn build_timeline(&self) -> Vec { diff --git a/crates/terraphim-session-analyzer/src/patterns/matcher.rs b/crates/terraphim-session-analyzer/src/patterns/matcher.rs index ec2ce94..ce3ef50 100644 --- a/crates/terraphim-session-analyzer/src/patterns/matcher.rs +++ b/crates/terraphim-session-analyzer/src/patterns/matcher.rs @@ -33,7 +33,7 @@ pub trait PatternMatcher: Send + Sync { /// /// Trait method consumed only by in-file unit tests in this module. /// External callers don't currently use it, hence the conditional allow. - /// Trait method consumed only by in-file unit tests in this module. External callers do not currently invoke it. + /// Trait method consumed only by in-file unit tests in this module. External callers do not currently invoke it. #[allow(dead_code)] fn matcher_type(&self) -> &'static str; } diff --git a/crates/terraphim_agent/src/client.rs b/crates/terraphim_agent/src/client.rs index 9ab59c1..408cb9f 100644 --- a/crates/terraphim_agent/src/client.rs +++ b/crates/terraphim_agent/src/client.rs @@ -228,7 +228,6 @@ pub struct AutocompleteResponse { // does not enable `firecracker`, so the lint sees them as dead. See // `Cargo.toml` [features] for the firecracker declaration. - #[derive(Debug, Serialize, Deserialize, Clone)] // Feature-gated to `firecracker`; see VM Management Types comment above. #[allow(dead_code)] @@ -430,8 +429,6 @@ impl ApiClient { Ok(body) } - - // VM Management APIs // Feature-gated to `firecracker`; see VM Management Types comment above. diff --git a/crates/terraphim_agent/src/learnings/capture.rs b/crates/terraphim_agent/src/learnings/capture.rs index 1a3677f..f5f86ab 100644 --- a/crates/terraphim_agent/src/learnings/capture.rs +++ b/crates/terraphim_agent/src/learnings/capture.rs @@ -1620,7 +1620,11 @@ pub fn shared_learning_from_entry( let mut kws: Vec = Vec::with_capacity(l.tags.len() + l.entities.len()); kws.extend(l.tags.iter().cloned()); kws.extend(l.entities.iter().cloned()); - (body, kws, terraphim_types::shared_learning::LearningSource::BashHook) + ( + body, + kws, + terraphim_types::shared_learning::LearningSource::BashHook, + ) } LearningEntry::Correction(c) => { let body = format!( @@ -1632,14 +1636,14 @@ pub fn shared_learning_from_entry( "correction".to_string(), ]; kws.extend(c.tags.iter().cloned()); - (body, kws, terraphim_types::shared_learning::LearningSource::Manual) + ( + body, + kws, + terraphim_types::shared_learning::LearningSource::Manual, + ) } LearningEntry::Procedure(p) => { - let steps: Vec = p - .steps - .iter() - .map(|s| format!("- {}", s.command)) - .collect(); + let steps: Vec = p.steps.iter().map(|s| format!("- {}", s.command)).collect(); let body = format!( "Procedure: {}\nDescription: {}\nSteps ({}):\n{}", p.title, @@ -1649,7 +1653,11 @@ pub fn shared_learning_from_entry( ); let mut kws = vec!["procedure".to_string()]; kws.extend(p.tags.iter().cloned()); - (body, kws, terraphim_types::shared_learning::LearningSource::Manual) + ( + body, + kws, + terraphim_types::shared_learning::LearningSource::Manual, + ) } }; @@ -2922,12 +2930,7 @@ mod tests { LearningEntry::Learning(learning) } - fn fixed_correction( - id: &str, - original: &str, - corrected: &str, - tags: &[&str], - ) -> LearningEntry { + fn fixed_correction(id: &str, original: &str, corrected: &str, tags: &[&str]) -> LearningEntry { let mut c = CorrectionEvent::new( CorrectionType::ToolPreference, original.to_string(), @@ -3078,9 +3081,7 @@ mod tests { ); for entry in [&one_hit, &two_hit, &three_hit] { let path = match entry { - LearningEntry::Learning(l) => { - storage.join(format!("learning-{}.md", l.id)) - } + LearningEntry::Learning(l) => storage.join(format!("learning-{}.md", l.id)), _ => unreachable!(), }; fs::write( @@ -3128,11 +3129,7 @@ mod tests { // `len() > 2` filter, so the scorer falls back to recent-by-time. let entry = fixed_learning("FALLBACK-1", "ls -la", "ok", &[]); if let LearningEntry::Learning(l) = &entry { - fs::write( - storage.join("learning-fb.md"), - l.to_markdown(), - ) - .unwrap(); + fs::write(storage.join("learning-fb.md"), l.to_markdown()).unwrap(); } // Context "a i" → after `len() > 2` filter, no keywords remain. @@ -3162,8 +3159,8 @@ mod tests { fn test_shared_learning_from_entry_converts_learning_variant() { let entry = fixed_learning("FRESH-1", "git push -f", "remote: rejected", &["git"]); let shared_ids = std::collections::HashSet::new(); - let shared = shared_learning_from_entry(&entry, &shared_ids) - .expect("fresh id should be retained"); + let shared = + shared_learning_from_entry(&entry, &shared_ids).expect("fresh id should be retained"); assert_eq!(shared.id, "FRESH-1"); assert_eq!(shared.source_agent, "legacy-local"); assert!(matches!( @@ -3199,8 +3196,8 @@ mod tests { entry_unwrapped.correction = Some("git push origin main".to_string()); let entry = LearningEntry::Learning(entry_unwrapped); let shared_ids = std::collections::HashSet::new(); - let shared = shared_learning_from_entry(&entry, &shared_ids) - .expect("fresh id should be retained"); + let shared = + shared_learning_from_entry(&entry, &shared_ids).expect("fresh id should be retained"); assert_eq!(shared.id, "FRESH-2"); assert!( shared @@ -3213,12 +3210,7 @@ mod tests { #[test] fn test_shared_learning_from_entry_converts_correction_variant() { - let entry = fixed_correction( - "FRESH-3", - "npm install", - "bun add", - &["tool"], - ); + let entry = fixed_correction("FRESH-3", "npm install", "bun add", &["tool"]); let shared_ids = std::collections::HashSet::new(); let shared = shared_learning_from_entry(&entry, &shared_ids) .expect("correction id should be retained"); diff --git a/crates/terraphim_agent/src/main.rs b/crates/terraphim_agent/src/main.rs index 38dbb72..b3988ed 100644 --- a/crates/terraphim_agent/src/main.rs +++ b/crates/terraphim_agent/src/main.rs @@ -3934,18 +3934,13 @@ async fn run_suggest_command(sub: SuggestSub) -> Result<()> { .map_err(|e| anyhow::anyhow!("{}", e))?; // 2. Keyword scoring across the local legacy corpus. - let local_scored = crate::learnings::capture::suggest_learnings( - &local_storage_dir, - ctx, - 5, - ) - .unwrap_or_default(); + let local_scored = + crate::learnings::capture::suggest_learnings(&local_storage_dir, ctx, 5) + .unwrap_or_default(); // 3. De-duplicate against the shared index. - let shared_ids: std::collections::HashSet = shared_top - .iter() - .map(|l| l.id.clone()) - .collect(); + let shared_ids: std::collections::HashSet = + shared_top.iter().map(|l| l.id.clone()).collect(); let local_candidates: Vec<(f64, _)> = local_scored .into_iter() .filter_map(|se| { @@ -3961,12 +3956,7 @@ async fn run_suggest_command(sub: SuggestSub) -> Result<()> { // 4. Merge and rank. let merged = store - .suggest_with_local_scored( - ctx, - "session-end", - local_candidates, - 1, - ) + .suggest_with_local_scored(ctx, "session-end", local_candidates, 1) .await .map_err(|e| anyhow::anyhow!("{}", e))?; diff --git a/crates/terraphim_agent/tests/kg_ranking_integration_test.rs b/crates/terraphim_agent/tests/kg_ranking_integration_test.rs index 4b64264..959ed6d 100644 --- a/crates/terraphim_agent/tests/kg_ranking_integration_test.rs +++ b/crates/terraphim_agent/tests/kg_ranking_integration_test.rs @@ -463,7 +463,6 @@ async fn test_knowledge_graph_ranking_impact() -> Result<()> { } else { 0.0 }; - println!(" BM25 avg: {:.2}", bm25_avg); println!(" Title avg: {:.2}", title_avg); diff --git a/crates/terraphim_mcp_server/tests/test_all_mcp_tools.rs b/crates/terraphim_mcp_server/tests/test_all_mcp_tools.rs index 5d55fd8..8238808 100644 --- a/crates/terraphim_mcp_server/tests/test_all_mcp_tools.rs +++ b/crates/terraphim_mcp_server/tests/test_all_mcp_tools.rs @@ -68,7 +68,9 @@ fn test_all_mcp_tools() { println!("1. Sending initialization request..."); let line = format!("{}\n", init_request); - stdin.write_all(line.as_bytes()).expect("Failed to write to stdin"); + stdin + .write_all(line.as_bytes()) + .expect("Failed to write to stdin"); stdin.flush().expect("Failed to flush stdin"); let mut response = String::new(); @@ -94,7 +96,9 @@ fn test_all_mcp_tools() { println!("2. Sending initialized notification..."); let line = format!("{}\n", initialized_notification); - stdin.write_all(line.as_bytes()).expect("Failed to write notification"); + stdin + .write_all(line.as_bytes()) + .expect("Failed to write notification"); stdin.flush().expect("Failed to flush stdin"); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -110,7 +114,9 @@ fn test_all_mcp_tools() { println!("3. Listing available tools..."); let line = format!("{}\n", tools_request); - stdin.write_all(line.as_bytes()).expect("Failed to write to stdin"); + stdin + .write_all(line.as_bytes()) + .expect("Failed to write to stdin"); stdin.flush().expect("Failed to flush stdin"); response.clear(); @@ -133,18 +139,30 @@ fn test_all_mcp_tools() { println!("Number of tools available: {}", tools.len()); // `json_decode` is a pure JSON utility with no KG dependency. - exercise_call_tool(&mut stdin, &mut reader, "json_decode", - serde_json::json!({"jsonlines": "{\"a\":1}\n{\"b\":2}\n"})); + exercise_call_tool( + &mut stdin, + &mut reader, + "json_decode", + serde_json::json!({"jsonlines": "{\"a\":1}\n{\"b\":2}\n"}), + ); // `find_files` is a lightweight file-search that does not load the // thesaurus. We point it at the hermetic root so it returns quickly. - exercise_call_tool(&mut stdin, &mut reader, "find_files", - serde_json::json!({"query": "non-existent-prefix", "path": root.to_string_lossy(), "limit": 5})); + exercise_call_tool( + &mut stdin, + &mut reader, + "find_files", + serde_json::json!({"query": "non-existent-prefix", "path": root.to_string_lossy(), "limit": 5}), + ); // `grep_files` is also lightweight. An empty query against the hermetic // root returns no matches without spinning up the thesaurus. - exercise_call_tool(&mut stdin, &mut reader, "grep_files", - serde_json::json!({"query": "no-such-pattern-xyzzy", "path": root.to_string_lossy(), "limit": 5})); + exercise_call_tool( + &mut stdin, + &mut reader, + "grep_files", + serde_json::json!({"query": "no-such-pattern-xyzzy", "path": root.to_string_lossy(), "limit": 5}), + ); println!("Test completed!"); @@ -170,7 +188,9 @@ fn exercise_call_tool( println!("Calling {tool} with arguments {arguments}"); let line = format!("{}\n", request); - stdin.write_all(line.as_bytes()).expect("Failed to write to stdin"); + stdin + .write_all(line.as_bytes()) + .expect("Failed to write to stdin"); stdin.flush().expect("Failed to flush stdin"); let mut response = String::new(); @@ -179,10 +199,9 @@ fn exercise_call_tool( .expect("Failed to read response"); println!("{tool} response: '{}'", response.trim()); - let value: Value = - serde_json::from_str(&response).unwrap_or_else(|e| panic!( - "{tool} response must be valid JSON, got error {e}: {response}" - )); + let value: Value = serde_json::from_str(&response).unwrap_or_else(|e| { + panic!("{tool} response must be valid JSON, got error {e}: {response}") + }); // tools/call returns either a `result` (success or structured error // content) or `error`. Either is acceptable; we just verify the // response is well-formed JSON-RPC. @@ -190,4 +209,4 @@ fn exercise_call_tool( value.get("result").is_some() || value.get("error").is_some(), "{tool} response missing result/error: {response}" ); -} \ No newline at end of file +} diff --git a/crates/terraphim_mcp_server/tests/test_tools_list.rs b/crates/terraphim_mcp_server/tests/test_tools_list.rs index faa4db8..ddd7b74 100644 --- a/crates/terraphim_mcp_server/tests/test_tools_list.rs +++ b/crates/terraphim_mcp_server/tests/test_tools_list.rs @@ -87,9 +87,7 @@ fn test_tools_list_only() { child.wait().ok(); let _ = stderr_log_thread.join(); let log = stderr_log.lock().expect("stderr log mutex").clone(); - panic!( - "broken pipe writing initialize request ({e}); server stderr:\n{log}" - ); + panic!("broken pipe writing initialize request ({e}); server stderr:\n{log}"); } } stdin.flush().expect("Failed to flush stdin"); @@ -115,7 +113,9 @@ fn test_tools_list_only() { println!("2. Sending initialized notification..."); let line = format!("{}\n", initialized_notification); - stdin.write_all(line.as_bytes()).expect("Failed to write notification"); + stdin + .write_all(line.as_bytes()) + .expect("Failed to write notification"); stdin.flush().expect("Failed to flush stdin"); thread::sleep(std::time::Duration::from_millis(100)); @@ -129,7 +129,9 @@ fn test_tools_list_only() { println!("3. Listing available tools..."); let line = format!("{}\n", tools_request); - stdin.write_all(line.as_bytes()).expect("Failed to write to stdin"); + stdin + .write_all(line.as_bytes()) + .expect("Failed to write to stdin"); stdin.flush().expect("Failed to flush stdin"); response.clear(); From 98b381e82d162df6a9651d1ed0b815a7bfb5b3b8 Mon Sep 17 00:00:00 2001 From: Alex Mikheev Date: Fri, 11 Sep 2026 23:55:26 +0100 Subject: [PATCH 3/3] ci: coverage job tolerates test failures; record first-run findings (Refs #254) --ignore-run-fail on cargo llvm-cov so the coverage gate reports the number even when the test gate is red (#264). Document the GitHub and Gitea main lineage divergence, the private-registry block on the GitHub mirror, and the Gitea-side job skips seen on the first runs. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WjGks4DGYdDw3QLgu32S96 --- .gitea/workflows/native-ci.yml | 4 +++- .github/workflows/ci.yml | 5 +++-- docs/ci-gap-analysis.md | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/native-ci.yml b/.gitea/workflows/native-ci.yml index 805269c..f4d90f0 100644 --- a/.gitea/workflows/native-ci.yml +++ b/.gitea/workflows/native-ci.yml @@ -78,13 +78,15 @@ jobs: # steps: the runner allowlist rejects non-cargo programs). - run: cargo nextest run -p terraphim_agent --test packaged_install_graph_regression --test ci_guards --profile ci # Stage 3: coverage gate. Threshold only ever goes up (docs/ci-gap-analysis.md). + # --ignore-run-fail: the test job owns test failures; this job owns the + # percentage, so a red test must not hide the coverage number. coverage: needs: check runs-on: terraphim-native steps: - run: rustup component add llvm-tools - run: cargo install cargo-llvm-cov --locked - - run: cargo llvm-cov --workspace --lib --lcov --output-path lcov.info + - run: cargo llvm-cov --workspace --lib --lcov --output-path lcov.info --ignore-run-fail - run: cargo llvm-cov report --fail-under-lines 65 # Stage 4: UB gates on nightly (from the #252 UB_RUNBOOK). Miri covers the # pure-computation crates only; tokio/reqwest paths are unsupported by Miri. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 164c83f..f051988 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,8 @@ jobs: # #95: isolated packaged install-graph regression. - run: cargo nextest run -p terraphim_agent --test packaged_install_graph_regression --profile ci - # Stage 3: coverage gate (Linux only; threshold only ever increases) + # Stage 3: coverage gate (Linux only; threshold only ever increases). + # --ignore-run-fail: the test job owns test failures; this job owns the number. coverage: needs: check runs-on: ubuntu-latest @@ -75,7 +76,7 @@ jobs: components: llvm-tools - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@cargo-llvm-cov - - run: cargo llvm-cov --workspace --lib --lcov --output-path lcov.info + - run: cargo llvm-cov --workspace --lib --lcov --output-path lcov.info --ignore-run-fail - run: cargo llvm-cov report --fail-under-lines 65 - uses: actions/upload-artifact@v4 with: diff --git a/docs/ci-gap-analysis.md b/docs/ci-gap-analysis.md index 823a2ce..5aef179 100644 --- a/docs/ci-gap-analysis.md +++ b/docs/ci-gap-analysis.md @@ -84,6 +84,20 @@ Gate: `--fail-under-lines 65`. Per-crate targets from the skill's table (80 perc Miri cannot execute tokio, mio, reqwest or process spawning. The `ub-gates` job therefore runs the pure-computation crates only: terraphim_negative_contribution, terraphim_command_runtime and terraphim_hooks. The hooks `discovery` tests spawn a subprocess (`posix_spawnattr_init` is unsupported by Miri) and are skipped with `-- --skip discovery`, and the 1000-iteration latency test in the same crate is skipped with `--skip latency` because wall-clock assertions are meaningless under Miri. The #252 runbook widens this list as Phase 3 establishes which other test modules are Miri-clean (session-analyzer parsing and sessions redaction are the next candidates). +## Two lineages, not one repository + +GitHub `main` is not a mirror of Gitea `main`. At the time of writing GitHub is 17 commits ahead of and 253 behind the Gitea lineage, and the two do not merge cleanly. Consequences: + +1. A branch cut from Gitea `main` cannot be a GitHub pull request (it conflicts, and GitHub Actions skips conflicting PRs). The CI change therefore exists twice: `task/254-ci-pipeline` on the Gitea lineage and `task/254-ci-pipeline-gh` on the GitHub lineage, with identical CI files. +2. The GitHub lineage carried unformatted code in terraphim-session-analyzer (43 rustfmt diffs under 1.97.1); a whitespace-only commit fixes it there. +3. The GitHub mirror cannot resolve the private registry: the first real run failed with `failed to load source for dependency terraphim_service` because `https://git.terraphim.cloud/api/packages/terraphim/cargo/config.json` answered "Not available" to the GitHub runner. Until the 1.21.x family is on crates.io (#210) or the registry token secret is confirmed to work in pull-request runs, the GitHub `test`, `coverage` and `ub-gates` jobs cannot go green. This is a pre-existing condition that the old single-job workflow never reached because it failed at rustfmt first. + +## First-run observations on native-ci + +- `check` (fmt, clippy, cargo-deny including its install) took about 2.5 minutes; cargo-deny's `cargo install` alone was 84 seconds. A host-level install would remove that. +- `coverage` failed on the first run because `cargo llvm-cov` aborts when a test fails (#264); `--ignore-run-fail` now separates the two concerns. +- Gitea marked the `test` job of run 449 and the `check` job of run 451 as skipped at creation, with no runner having fetched the task (verified in the runner journal). No per-job rerun API exists on Gitea 1.26 (`jobs/{id}/rerun` and `runs/{id}/rerun` both 404), so a skipped job costs a full `workflow_dispatch`. Cause unknown; tracked with #244 and terraphim-ai#3375. + ## Sync rule `native-ci.yml` is the source of truth. Any change to a `cargo` invocation in a gate is made there first and mirrored into `ci.yml` in the same commit. Allowed divergence between the two: