Merge origin/main: reconcile #2432 with the merged #2580 family (#258… #4128
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: verify | |
| on: | |
| push: | |
| pull_request: | |
| permissions: {} | |
| env: | |
| # ubuntu-latest runners have 4 vCPUs and 16GB RAM. With line-tables-only | |
| # debug info + 4GB swap, we can compile with all 4 cores instead of 2, | |
| # cutting build wall-time roughly in half. Linker peak memory observed at | |
| # ~3.2GB even with all 4 cores active; well under the runner limit. | |
| CARGO_BUILD_JOBS: 4 | |
| # Reduce DWARF size to keep peak linker memory under runner limits. | |
| # Full debug info pushes the linker over the runner's RSS limit and the | |
| # kernel kills it with SIGBUS (signal 7) during the link step. | |
| CARGO_PROFILE_DEV_DEBUG: "line-tables-only" | |
| CARGO_PROFILE_DEV_INCREMENTAL: "false" | |
| CARGO_PROFILE_TEST_DEBUG: "line-tables-only" | |
| CARGO_PROFILE_TEST_INCREMENTAL: "false" | |
| # Speed up dependency compilation: disable debug info on dependencies (we | |
| # only need it for our own code). | |
| CARGO_PROFILE_DEV_BUILD_OVERRIDE_DEBUG: "false" | |
| CARGO_PROFILE_TEST_BUILD_OVERRIDE_DEBUG: "false" | |
| jobs: | |
| pre-commit: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Rust-only gate (no new .py / .js / .ts) | |
| run: scripts/check-rust-only-gate.sh | |
| - name: Prepare Rust runner (free disk, swap, mold, rust) | |
| uses: ./.github/actions/rust-runner-prep | |
| with: | |
| rust-components: rustfmt,clippy | |
| - name: Cache cargo registry and build artifacts | |
| uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| # Bumped 2026-05-10: previous `simard-ci` cache held stale test | |
| # binaries from before tests were renamed/deleted (notably | |
| # `engineer_loop_probe_fails_visibly_when_structured_replacement_target_is_missing` | |
| # deleted in 1fcd9bb0 and `version_string_is_semver` updated past | |
| # 0.16.1 → 0.17.0). Cargo's mtime-based fingerprint trusted the | |
| # restored .rmeta files and re-ran the dead tests against new | |
| # source. Bump key to force a clean rebuild. | |
| shared-key: simard-ci-v2 | |
| # Single writer: only the main branch refreshes the shared cache. | |
| # PR runs read it but never overwrite, eliminating cache thrashing | |
| # across concurrent PRs. | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| cache-on-failure: true | |
| # Persist the pre-commit clippy wrapper's stable lbug prebuilt so the | |
| # version-pinned download happens once per cache lifetime instead of | |
| # on every run (issue #2426). This dir lives outside registry/src on | |
| # purpose, so it isn't otherwise covered by the cargo cache. | |
| cache-directories: | | |
| ~/.cache/simard-lbug-precommit | |
| - name: Provision lbug native static library link path (#2426 / #2423) | |
| # Pin the lbug native-static-lib path for every cargo invocation in this | |
| # job. lbug 0.17.1 caches its prebuilt `liblbug.a` under registry/src, | |
| # which this cache restores around but evicts — so the cached release | |
| # build-script output points at a missing archive. Provision a stable | |
| # copy, point lbug at it via its external-lib interface, and drop the | |
| # stale cached lbug build outputs so the build script re-runs and adopts | |
| # the pinned path. Runs after the cache restore on purpose. | |
| shell: bash | |
| run: | | |
| lib_dir="$HOME/.cache/simard-lbug-precommit/lib" | |
| scripts/provision-lbug-prebuilt.sh "$lib_dir" >/dev/null | |
| echo "LBUG_LIBRARY_DIR=$lib_dir" >> "$GITHUB_ENV" | |
| echo "LBUG_INCLUDE_DIR=$lib_dir" >> "$GITHUB_ENV" | |
| rm -rf target/*/build/lbug-* target/*/.fingerprint/lbug-* 2>/dev/null || true | |
| - name: Set up Python | |
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.x" | |
| - name: Install pre-commit | |
| run: python -m pip install --upgrade pip pre-commit | |
| - name: Run pre-commit stage | |
| id: precommit_stage | |
| # `--verbose` so the cargo-clippy-precommit hook's output (including the | |
| # lbug native-lib resolution marker on stderr) is captured even on | |
| # success, which the regression-guard step below asserts on. Tee'd to a | |
| # log so the assertion runs without re-invoking the expensive release | |
| # clippy. | |
| run: | | |
| set -o pipefail | |
| mkdir -p target/ci-logs | |
| python -m pre_commit run --all-files --show-diff-on-failure --verbose \ | |
| --hook-stage pre-commit 2>&1 | tee target/ci-logs/precommit-stage.log | |
| - name: Assert pre-commit clippy compiled/linked lbug (regression guard #2426) | |
| # Guards against silent reintroduction of issue #2426 (pre-commit clippy | |
| # --release failing with "could not find native static library `lbug`"). | |
| # `if: always()` so the explicit #2426 annotation still fires when the | |
| # pre-commit stage failed (a real regression fails that step first, which | |
| # would otherwise skip this guard). Cheap: greps the captured log, no | |
| # recompilation. | |
| if: always() | |
| run: | | |
| outcome="${{ steps.precommit_stage.outcome }}" | |
| if [ "$outcome" != "success" ] && [ "$outcome" != "failure" ]; then | |
| echo "pre-commit stage did not run (outcome=$outcome); skipping #2426 guard" | |
| exit 0 | |
| fi | |
| log=target/ci-logs/precommit-stage.log | |
| test -f "$log" || { echo "::error::missing pre-commit stage log"; exit 1; } | |
| if grep -qiE "could not find native static library .?lbug" "$log"; then | |
| echo "::error::REGRESSION (#2426): pre-commit clippy --release failed to link the lbug native static library" | |
| exit 1 | |
| fi | |
| if [ "$outcome" != "success" ]; then | |
| echo "pre-commit stage failed for a non-lbug reason; see the failing hook above" | |
| exit 0 | |
| fi | |
| if ! grep -q "\[clippy-precommit-release\] SUCCESS: cargo clippy --release linked lbug" "$log"; then | |
| echo "::error::pre-commit clippy --release did not report linking lbug; the lbug link-path wrapper may have regressed" | |
| exit 1 | |
| fi | |
| echo "OK: pre-commit clippy --release compiled and linked lbug (issue #2426 guard passed)" | |
| - name: Run cargo test (streamed, captured for artifact) | |
| id: cargo_test | |
| run: | | |
| set -o pipefail | |
| mkdir -p target/ci-logs | |
| # Skip list (each entry must have a tracking issue): | |
| # install_packages_runs_and_self_installs — covered by the | |
| # dedicated `install-real` job below. The test does a full | |
| # `cargo install --path . --root <tmp>` from scratch and runs | |
| # ~25 min on a cold cache; running it twice (here AND in | |
| # install-real) doubles total verify wall time for no gain. | |
| cargo test --all-features --locked --no-fail-fast \ | |
| -- --skip install_packages_runs_and_self_installs \ | |
| 2>&1 | tee target/ci-logs/cargo-test.log | |
| - name: Run pre-push stage (cargo-clippy only) | |
| # Runs AFTER cargo test on purpose: test compiles the entire crate | |
| # graph (test profile) into target/debug/deps. Clippy on `--all-targets` | |
| # with a warm target dir reuses those .rmeta artifacts and takes | |
| # single-digit minutes instead of the ~17 min cold cost it pays when | |
| # run first. | |
| run: python -m pre_commit run --all-files --show-diff-on-failure --hook-stage pre-push cargo-clippy | |
| - name: Build minimal binary (--no-default-features contract, issue #2576) | |
| # Issue #2576 made every runtime feature (signal, dashboard-audit) part | |
| # of the default set, so the default `cargo build` below already exercises | |
| # the fully-capable path. This step locks the OTHER edge: that the crate | |
| # still compiles and links with NO features, i.e. the deliberately minimal | |
| # binary keeps building (the signal channel drops to its feature-off stub). | |
| # `--locked` guards against Cargo.lock churn on the feature-off resolution. | |
| run: cargo build --release --bin simard --no-default-features --locked | |
| - name: Build simard binary for downstream jobs | |
| # cargo test above already compiles the simard library + bins for the | |
| # test profile. Producing the dev-profile binary here is incremental | |
| # (~10s with hot cache) and lets install-real + e2e-dashboard skip | |
| # their own from-scratch builds entirely. | |
| run: cargo build --bin simard --locked | |
| - name: Upload simard binary artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: simard-bin-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: target/debug/simard | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Upload cargo-test log | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: cargo-test-log-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: target/ci-logs/cargo-test.log | |
| if-no-files-found: warn | |
| retention-days: 7 | |
| cargo-audit: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Install cargo-audit | |
| uses: taiki-e/install-action@754bf4dbae00ad1b16b244717154b96ba27d2416 # cargo-audit | |
| - name: Run cargo audit | |
| run: cargo audit | |
| cargo-deny: | |
| # Supply-chain policy gate (issue #2260): advisories + licenses + bans + | |
| # sources, enforced by the repo-root deny.toml. Lockfile-only — it reads | |
| # Cargo.lock + deny.toml and never compiles the crate, so it is a separate | |
| # job (not folded into the memory-sensitive pre-commit build) and never | |
| # writes the shared rust-cache. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Install cargo-deny | |
| # SHA pins the per-tool `cargo-deny` tag of taiki-e/install-action | |
| # (same pattern as the cargo-audit job above). The `tool:` input also | |
| # pins the cargo-deny VERSION: deny.toml uses the 0.19.x advisories | |
| # schema, so an unpinned bump could reinterpret the policy. | |
| uses: taiki-e/install-action@4e4e4d1450e58bef95d6f394ac20d46ad7d24ebf # cargo-deny | |
| with: | |
| tool: cargo-deny@0.19.9 | |
| - name: Run cargo deny check | |
| # `--locked` is a top-level flag (before the subcommand): it fails on a | |
| # dirty Cargo.lock so the check always reflects the committed graph. | |
| run: cargo deny --locked check | |
| cargo-vet: | |
| # Transitive-dependency trust gate (issue #2262): every third-party crate | |
| # in the locked graph must be covered by an audit (imported or local) or an | |
| # explicit exemption recorded under supply-chain/. Lockfile-only, like the | |
| # cargo-audit and cargo-deny jobs. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Install cargo-vet | |
| # SHA pins the per-tool `cargo-vet` tag of taiki-e/install-action. | |
| uses: taiki-e/install-action@7b51dc7487ebab790625f16f2c5f541029a3b0ab # cargo-vet | |
| with: | |
| tool: cargo-vet | |
| - name: Run cargo vet | |
| # `--locked` fails on a dirty Cargo.lock; the baseline under | |
| # supply-chain/ records the current graph as exemptions so this is | |
| # green on day one and ratchets forward. | |
| run: cargo vet --locked | |
| npm-audit: | |
| # JS-dependency audit gate: the Rust graph is covered by cargo-audit / | |
| # cargo-deny / cargo-vet above, but the npm surface (package-lock.json, | |
| # dev-dep @playwright/test consumed by the e2e jobs) had no audit. Mirrors | |
| # the cargo-audit job; lockfile-only (`npm audit` reads package-lock.json | |
| # directly, so no `npm ci` install step is required). | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Set up Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: "20" | |
| - name: Run npm audit | |
| # Fails the build on any HIGH or CRITICAL advisory in the committed | |
| # package-lock.json (checked against the GitHub Advisory Database). | |
| run: npm audit --audit-level=high | |
| install-real: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| needs: pre-commit | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Prepare Rust runner (free disk, swap, mold, rust) | |
| uses: ./.github/actions/rust-runner-prep | |
| - name: Cache cargo registry and build artifacts | |
| uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| shared-key: simard-ci-v2 | |
| # Read-only on PRs and non-pre-commit jobs; pre-commit on main is | |
| # the single writer to avoid cache contention. | |
| save-if: false | |
| cache-on-failure: true | |
| cache-directories: | | |
| ~/.cache/simard-lbug-precommit | |
| - name: Provision lbug native static library link path (#2426 / #2423) | |
| # `cargo test --test install_real` (and the `cargo install` it shells | |
| # out to) build lbug and hit the same evicted registry-src prebuilt as | |
| # the pre-commit clippy gate. Pin the link path for the whole job so | |
| # both link lbug deterministically, regardless of profile. | |
| shell: bash | |
| run: | | |
| lib_dir="$HOME/.cache/simard-lbug-precommit/lib" | |
| scripts/provision-lbug-prebuilt.sh "$lib_dir" >/dev/null | |
| echo "LBUG_LIBRARY_DIR=$lib_dir" >> "$GITHUB_ENV" | |
| echo "LBUG_INCLUDE_DIR=$lib_dir" >> "$GITHUB_ENV" | |
| rm -rf target/*/build/lbug-* target/*/.fingerprint/lbug-* 2>/dev/null || true | |
| - name: Run install verification (cargo install + --version + ensure-deps + self-install) | |
| env: | |
| # Linker OOMs (signal 7) when embedding full debug info for the | |
| # `simard` binary under test on the 7GB GH runner. Line tables | |
| # keep backtraces working at a fraction of the size. | |
| CARGO_PROFILE_DEV_DEBUG: "line-tables-only" | |
| CARGO_PROFILE_DEV_INCREMENTAL: "false" | |
| run: cargo test --test install_real -- --nocapture | |
| e2e-dashboard: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| # Reuse the simard binary built by the pre-commit job instead of doing a | |
| # full from-scratch Rust build here. Cuts ~15-20 min off this job. | |
| needs: pre-commit | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Set up Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: "20" | |
| - name: Install npm dependencies | |
| run: npm ci | |
| - name: Install Playwright (chromium) | |
| run: npx playwright install --with-deps chromium | |
| - name: Download simard binary from pre-commit job | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: simard-bin-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: target/debug | |
| - name: Make simard binary executable | |
| run: chmod +x target/debug/simard | |
| - name: Provision dashkey | |
| run: | | |
| mkdir -p "$HOME/.simard" | |
| # 8-char synthetic dashkey for structural test fixture. | |
| printf 'testkey1' > "$HOME/.simard/.dashkey" | |
| - name: Run structural e2e tests | |
| env: | |
| SIMARD_BIN: ${{ github.workspace }}/target/debug/simard | |
| SIMARD_DASHKEY: testkey1 | |
| CI: "true" | |
| # Scope to overview.spec.ts plus the Logs level-filter regression | |
| # (logs-filter.spec.ts) — both run fully mocked and need no LLM | |
| # backend. Other structural specs (chat-lifecycle, multi-turn) | |
| # require an LLM backend that is intentionally not provisioned in CI; | |
| # they are validated separately when credentials are available. | |
| run: npx playwright test --config=tests/e2e-dashboard/playwright.config.ts --project=structural tests/e2e-dashboard/specs/overview.spec.ts tests/e2e-dashboard/specs/logs-filter.spec.ts | |
| # ---- Python tab-identity smoke test (#1993 / #1994 / #1995) ---- | |
| # Reuses the binary the TypeScript step downloaded plus the same | |
| # ~/.simard/.dashkey provisioned above. The dashboard server is | |
| # launched here on a dedicated port so we don't race the TS suite's | |
| # webServer lifecycle. | |
| - name: Set up Python for tab-identity smoke test | |
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.x" | |
| - name: Install tab-identity smoke-test dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r tests/e2e-dashboard/smoke_python/requirements.txt | |
| python -m playwright install chromium | |
| - name: Run tab-identity smoke test | |
| env: | |
| SIMARD_DASHKEY: testkey1 | |
| SIMARD_DASHBOARD_URL: http://localhost:18801 | |
| run: | | |
| # Launch the dashboard on a dedicated port and wait for it to | |
| # accept connections before invoking pytest. The PID is recorded | |
| # so we can shut it down cleanly when the smoke test finishes. | |
| target/debug/simard dashboard serve --port=18801 \ | |
| > /tmp/simard-dashboard-smoke.log 2>&1 & | |
| DASH_PID=$! | |
| echo "dashboard PID=$DASH_PID" | |
| # Wait up to 30 s for the server to start serving HTTP. | |
| for i in $(seq 1 30); do | |
| if curl -s -o /dev/null -w "%{http_code}" http://localhost:18801/login | grep -q '^200$'; then | |
| echo "dashboard ready after ${i}s" | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| # Run the smoke test. `set +e` so we always stop the dashboard | |
| # afterwards even if pytest fails. | |
| set +e | |
| pytest tests/e2e-dashboard/smoke_python/ -v --tb=short -s | |
| rc=$? | |
| set -e | |
| echo "--- dashboard log tail ---" | |
| tail -50 /tmp/simard-dashboard-smoke.log || true | |
| kill "$DASH_PID" 2>/dev/null || true | |
| exit $rc | |
| - name: Upload Playwright report on failure | |
| if: failure() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: playwright-report-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: | | |
| playwright-report/ | |
| test-results/ | |
| if-no-files-found: ignore | |
| retention-days: 7 |