Skip to content

Commit fb5b855

Browse files
Your Nameclaude
andcommitted
build(ci): add scripts/ci-local.sh — one command mirroring every blocking CI gate
CI is split into ~11 parallel jobs, so before a push there was no single command reproducing every blocking gate: a "green locally" push could still fail CI on a check the dev never ran. That is exactly what happened to 0a7a8aa (passed local `cargo test`, failed CI on fmt + clippy type_complexity + doc-truth, took 0a7a8aa->b1fd2a6->903d3ef to go green). ci-local.sh runs every gate in one pass and reports ALL failures at once: --lint fmt + all clippy feature-sets + doc/status/claims checks (default) + cargo test --workspace --full + feature-matrix tests, stack-graphs corpus, b2 thresholds, adr staleness, cargo audit, fitness-check, txn crash injection Deliberately NOT wired into ci.yml: CI keeps its parallel-job structure (per-job runners/feature-sets/rust-cache); the script mirrors those jobs with an explicit job->phase mapping in its header so drift stays visible in review. First PR of the evidence-architecture plan (docs/plans/2026-08-19-evidence-architecture-execution-plan.md, Wave 0.1). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 903d3ef commit fb5b855

2 files changed

Lines changed: 170 additions & 0 deletions

File tree

CONTRIBUTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ cargo test --workspace # embeddings is on by default, no extra --features flag
3636

3737
## Before opening a PR
3838

39+
Run `scripts/ci-local.sh` — it reproduces every blocking CI gate in one command
40+
and reports *all* failures at once, instead of finding them one push at a time:
41+
42+
- `scripts/ci-local.sh` — default: fmt + all clippy feature-sets + doc-truth + `cargo test --workspace` (fast, offline)
43+
- `scripts/ci-local.sh --lint` — lint + doc checks only (fastest pre-push gate)
44+
- `scripts/ci-local.sh --full` — everything CI runs, including the feature-matrix tests, fitness-check, and crash-injection
45+
46+
The individual gates it wraps, if you'd rather run them by hand:
47+
3948
- `cargo fmt` and `cargo clippy` clean
4049
- `cargo test --workspace` passes
4150
- `calm fitness-check --project-root .` passes against this repo's own thresholds — CALM reviewing itself is part of the point

scripts/ci-local.sh

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env bash
2+
#
3+
# ci-local.sh — run the SAME blocking checks CI runs, locally, in one command.
4+
#
5+
# WHY THIS EXISTS
6+
# CI (.github/workflows/ci.yml) is split into ~11 parallel jobs so it stays
7+
# fast on GitHub's runners. That parallelism is good for CI but bad for a
8+
# human: before a push there was no single command that reproduced every
9+
# blocking gate, so a "green locally" push could still fail CI on a check
10+
# the dev never ran (real example: 0a7a8aa passed a local `cargo test` but
11+
# failed CI on fmt + clippy(type_complexity) + doc-truth, taking three
12+
# pushes — 0a7a8aa -> b1fd2a6 -> 903d3ef — to go green). This script is the
13+
# fix: ONE command, run every gate, report ALL failures at once instead of
14+
# one-per-push.
15+
#
16+
# It is intentionally NOT wired into ci.yml. CI keeps its parallel-job
17+
# structure (different runners, feature sets, and rust-cache scopes per job);
18+
# forcing every job through one serial script would lose that. Instead this
19+
# script mirrors those jobs and the mapping is kept explicit below, so if a
20+
# CI job is added/changed, the drift is visible here in review.
21+
#
22+
# CI JOB MAPPING (ci.yml -> phase here)
23+
# verify (fmt/clippy/test/audit) ...... LINT + TEST (+ audit in --full)
24+
# embeddings .......................... LINT (clippy) + TEST_MATRIX
25+
# no-stack-graphs-formal .............. LINT (clippy + ts-unpinned) + TEST_MATRIX
26+
# all-languages ....................... LINT (clippy) + TEST_MATRIX
27+
# otel-http-features .................. LINT (clippy) + TEST_MATRIX (+ skew guard)
28+
# stack-graphs-corpus ................. TEST_MATRIX
29+
# status-drift ........................ DOCS
30+
# fitness-check ....................... HEAVY (--full)
31+
# txn-crash-injection ................. HEAVY (--full)
32+
# js-client-interop ................... HEAVY (--full, needs npm)
33+
# scip-nightly: check-b2-thresholds ... HEAVY (--full)
34+
# calm-guard-dogfood .................. NOT MIRRORED (continue-on-error shadow job)
35+
#
36+
# USAGE
37+
# scripts/ci-local.sh # default: LINT + DOCS + TEST (fast, offline)
38+
# scripts/ci-local.sh --lint # LINT + DOCS only (fastest pre-push gate)
39+
# scripts/ci-local.sh --full # everything, == full CI (slow; some steps need network/npm)
40+
#
41+
# Runs every selected check even if an earlier one fails (does NOT stop on
42+
# first error), then prints a summary and exits non-zero if any failed.
43+
44+
set -uo pipefail
45+
46+
cd "$(dirname "$0")/.." || exit 2
47+
export CARGO_TERM_COLOR="${CARGO_TERM_COLOR:-always}"
48+
49+
MODE="default"
50+
case "${1:-}" in
51+
--lint) MODE="lint" ;;
52+
--full) MODE="full" ;;
53+
""|--default) MODE="default" ;;
54+
-h|--help)
55+
sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'
56+
exit 0 ;;
57+
*)
58+
echo "unknown argument: $1 (use --lint | --default | --full | --help)" >&2
59+
exit 2 ;;
60+
esac
61+
62+
# ---- result collection --------------------------------------------------
63+
PASS=(); FAIL=()
64+
run_check() {
65+
local label="$1"; shift
66+
printf '\n\033[1;34m==> %s\033[0m\n' "$label"
67+
printf ' $ %s\n' "$*"
68+
if "$@"; then
69+
PASS+=("$label")
70+
else
71+
FAIL+=("$label")
72+
printf '\033[1;31m FAILED: %s\033[0m\n' "$label"
73+
fi
74+
}
75+
76+
# The 11-language + lsp feature string the all-languages CI job uses, kept in
77+
# one place so it can't drift between the clippy and test invocations.
78+
ALL_LANG_FEATURES="tier0-5,lang-kotlin,lang-swift,lang-scala,lang-dart,lang-lua,lang-elixir,lang-haskell,lang-ocaml,lang-zig,lang-powershell,lang-groovy,scip-overlay,lsp-overlay"
79+
NO_SG_FEATURES="embeddings,tier0-5,scip-overlay"
80+
81+
# Verify tree-sitter core is not pulled in transitively by the no-default
82+
# build (mirrors ci.yml no-stack-graphs-formal's "unpinned" guard).
83+
check_tree_sitter_unpinned() {
84+
local leaked
85+
leaked=$(cargo tree -p calm-core --no-default-features --features "$NO_SG_FEATURES" 2>&1 | grep -ic "stack-graph" || true)
86+
if [ "$leaked" -ne 0 ]; then
87+
echo "stack-graph* leaked into the no-stack-graphs-formal build ($leaked line(s))" >&2
88+
return 1
89+
fi
90+
echo "ok: no stack-graph* in no-default build"
91+
}
92+
93+
# Guard against opentelemetry core version skew (mirrors otel-http-features).
94+
check_otel_skew() {
95+
local versions
96+
versions=$(cargo metadata --format-version 1 --features otel 2>/dev/null \
97+
| jq -r '.packages[] | select(.name == "opentelemetry") | .version' | sort -u)
98+
local n
99+
n=$(printf '%s\n' "$versions" | grep -c . || true)
100+
if [ "$n" -gt 1 ]; then
101+
echo "opentelemetry appears at >1 version (skew): $versions" >&2
102+
return 1
103+
fi
104+
echo "ok: opentelemetry single version: ${versions:-<none>}"
105+
}
106+
107+
# ---- LINT (always) ------------------------------------------------------
108+
run_check "fmt --check" cargo fmt --all -- --check
109+
run_check "clippy (workspace)" cargo clippy --workspace --all-targets -- -D warnings
110+
run_check "clippy (embeddings)" cargo clippy -p calm-core --all-targets --features embeddings -- -D warnings
111+
run_check "clippy (no-stack-graphs)" cargo clippy -p calm-core --all-targets --no-default-features --features "$NO_SG_FEATURES" -- -D warnings
112+
run_check "clippy (all-languages+lsp)" cargo clippy --workspace --all-targets --features "$ALL_LANG_FEATURES" -- -D warnings
113+
run_check "clippy (otel)" cargo clippy -p calm-cli --features otel -- -D warnings
114+
run_check "clippy (http)" cargo clippy -p calm-server -p calm-cli --features http -- -D warnings
115+
run_check "tree-sitter unpinned guard" check_tree_sitter_unpinned
116+
117+
# ---- DOCS (always) ------------------------------------------------------
118+
run_check "status.generated.md fresh" ./scripts/gen-status.sh --check
119+
run_check "hand-authored doc truth" ./scripts/check-doc-truth.sh
120+
run_check "claims registry consistent" ./scripts/check-claims-registry.sh
121+
122+
# ---- TEST (default + full) ---------------------------------------------
123+
if [ "$MODE" != "lint" ]; then
124+
run_check "test (workspace)" cargo test --workspace
125+
fi
126+
127+
# ---- TEST_MATRIX + HEAVY (full only) -----------------------------------
128+
if [ "$MODE" = "full" ]; then
129+
run_check "test (embeddings)" cargo test -p calm-core --features embeddings
130+
run_check "test (no-stack-graphs)" cargo test -p calm-core --no-default-features --features "$NO_SG_FEATURES"
131+
run_check "test (all-languages+lsp)" cargo test --workspace --features "$ALL_LANG_FEATURES"
132+
run_check "test (otel)" cargo test -p calm-cli --features otel
133+
run_check "otel version-skew guard" check_otel_skew
134+
run_check "test (http)" cargo test -p calm-server -p calm-cli --features http
135+
run_check "stack-graphs corpus" cargo test --test parity_test test_formal_edges -- --nocapture
136+
run_check "b2 thresholds" ./scripts/check-b2-thresholds.sh
137+
run_check "adr staleness" ./scripts/check-adr-staleness.sh
138+
# Networked / heaviest — only in --full, and tolerated-missing where a tool
139+
# isn't installed rather than silently skipped.
140+
if command -v cargo-audit >/dev/null 2>&1; then
141+
run_check "cargo audit" cargo audit
142+
else
143+
echo "note: cargo-audit not installed; run 'cargo install cargo-audit --locked' to include it" >&2
144+
fi
145+
run_check "fitness-check (build+index)" bash -c '
146+
cargo build --bin calm &&
147+
./target/debug/calm index --project-root . &&
148+
./target/debug/calm fitness-check --project-root . --config thresholds.toml'
149+
run_check "txn crash injection" cargo test -p calm-cli --test txn_crash_injection -- --ignored
150+
fi
151+
152+
# ---- summary ------------------------------------------------------------
153+
printf '\n\033[1m================ ci-local summary (mode: %s) ================\033[0m\n' "$MODE"
154+
for p in "${PASS[@]:-}"; do [ -n "$p" ] && printf ' \033[1;32mPASS\033[0m %s\n' "$p"; done
155+
for f in "${FAIL[@]:-}"; do [ -n "$f" ] && printf ' \033[1;31mFAIL\033[0m %s\n' "$f"; done
156+
printf '\033[1m%s passed, %s failed\033[0m\n' "${#PASS[@]}" "${#FAIL[@]}"
157+
158+
if [ "${#FAIL[@]}" -ne 0 ]; then
159+
exit 1
160+
fi
161+
echo "all selected CI checks passed."

0 commit comments

Comments
 (0)