Skip to content

Merge origin/main: reconcile #2432 with the merged #2580 family (#258… #4128

Merge origin/main: reconcile #2432 with the merged #2580 family (#258…

Merge origin/main: reconcile #2432 with the merged #2580 family (#258… #4128

Workflow file for this run

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