26382a7ac6
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
JetBrains Plugin / Actionlint (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
JetBrains Plugin / Validation (push) Waiting to run
JetBrains Plugin / Build (push) Waiting to run
JetBrains Plugin / Test (push) Blocked by required conditions
Security Check / Security Scan (push) Waiting to run
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
290 lines
12 KiB
Bash
Executable File
290 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# Local CI-parity gate — mirrors .github/workflows/ci.yml.
|
||
#
|
||
# A green run here means the *deterministic* CI jobs (Format, Clippy,
|
||
# Documentation, and the cross-platform compile) will pass. It exists because
|
||
# those failures otherwise only surface after a full ~50-min CI matrix:
|
||
# e.g. a private intra-doc link (Documentation job) or test-only code that is
|
||
# dead on Windows (Test job) — both invisible to `cargo test` / plain clippy.
|
||
#
|
||
# Change-aware (#850): the Rust gates can only be broken by Rust/Cargo changes,
|
||
# so a docs-only push (README, CHANGELOG, *.md, website, …) skips them and the
|
||
# pre-push gate finishes in a second. CI still runs every job unconditionally —
|
||
# it stays the source of truth — but a docs-only diff cannot turn a Rust gate
|
||
# red, so skipping it locally can never produce a local-green / CI-red split.
|
||
# `gen_docs --check` additionally guards the committed generated reference, so it
|
||
# also runs whenever a file under docs/reference/generated/** changed by hand.
|
||
# `full` (release parity) ignores classification and runs everything + tests.
|
||
#
|
||
# No-test policy (#849): a change to contract code (proxy / tools / config
|
||
# schema — anything feeding deterministic output, #498) that carries no test
|
||
# signal is flagged. Advisory by default; LEAN_CTX_PREFLIGHT_STRICT_TESTS=1
|
||
# makes it blocking.
|
||
#
|
||
# Usage:
|
||
# scripts/preflight.sh [fast|full] (default: fast)
|
||
# fast change-aware: fmt + clippy + doc + gen_docs drift + Windows
|
||
# cross-compile, each skipped when no Rust/generated-doc file changed
|
||
# full force everything regardless of the diff, plus `cargo test --lib`
|
||
#
|
||
# Bypass: not from here — use `SKIP_PREFLIGHT=1 git push` / `git push --no-verify`.
|
||
#
|
||
# CI parity (.github/workflows/ci.yml):
|
||
# - global env: RUSTFLAGS=-Dwarnings, LEAN_CTX_EMBEDDINGS_AUTO_DOWNLOAD=0
|
||
# - Documentation job: RUSTDOCFLAGS=-Dwarnings cargo doc --no-deps --all-features
|
||
# cargo run --example gen_docs --features dev-tools -- --check
|
||
# - Clippy job: cargo clippy --all-features -- -D warnings
|
||
# - Format job: cargo fmt --check
|
||
# - Test job (Windows) compiles for x86_64-pc-windows-gnu
|
||
|
||
set -o pipefail
|
||
|
||
LEVEL="${1:-fast}"
|
||
case "$LEVEL" in
|
||
fast|full) ;;
|
||
*) echo "usage: $0 [fast|full]" >&2; exit 2 ;;
|
||
esac
|
||
|
||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
|
||
# Base ref for changed-file detection. Overridable for forks/CI that track a
|
||
# different upstream; defaults to the branch CI gates against.
|
||
BASE_REF="${PREFLIGHT_BASE:-origin/main}"
|
||
|
||
# ── Change classification (#850/#849) ─────────────────────────────────
|
||
# Sets: RUST_CHANGED, GENERATED_DOCS_CHANGED, CONTRACT_CHANGED, TEST_SIGNAL,
|
||
# CHANGED_COUNT, CLASSIFY_OK. When the base ref is unreachable (fresh clone,
|
||
# detached history) CLASSIFY_OK=0 → callers fall back to running everything.
|
||
classify_changes() {
|
||
RUST_CHANGED=0
|
||
GENERATED_DOCS_CHANGED=0
|
||
CONTRACT_CHANGED=0
|
||
CCR_CHANGED=0
|
||
TEST_SIGNAL=0
|
||
CHANGED_COUNT=0
|
||
CLASSIFY_OK=1
|
||
BASE_SHA=""
|
||
|
||
local base
|
||
base="$(git -C "$REPO_ROOT" merge-base "$BASE_REF" HEAD 2>/dev/null || true)"
|
||
if [[ -z "$base" ]]; then
|
||
CLASSIFY_OK=0
|
||
return
|
||
fi
|
||
BASE_SHA="$base"
|
||
|
||
# base..working-tree (committed + staged + unstaged) ∪ untracked.
|
||
local files
|
||
files="$( {
|
||
git -C "$REPO_ROOT" diff --name-only "$base"
|
||
git -C "$REPO_ROOT" ls-files --others --exclude-standard
|
||
} 2>/dev/null | sed 's#\\#/#g' | sort -u | sed '/^$/d' )"
|
||
|
||
CHANGED_COUNT="$(printf '%s\n' "$files" | sed '/^$/d' | grep -c '' || true)"
|
||
|
||
local f
|
||
while IFS= read -r f; do
|
||
[[ -z "$f" ]] && continue
|
||
case "$f" in
|
||
rust/*) RUST_CHANGED=1 ;;
|
||
esac
|
||
case "$f" in
|
||
docs/reference/generated/*) GENERATED_DOCS_CHANGED=1 ;;
|
||
esac
|
||
case "$f" in
|
||
rust/src/proxy/*|rust/src/tools/*|rust/src/core/config/schema/*) CONTRACT_CHANGED=1 ;;
|
||
esac
|
||
# CCR recovery surface (#983): a change here must keep the robustness suite
|
||
# green, so flag it for the change-aware `cargo test --lib ccr_robustness` gate.
|
||
case "$f" in
|
||
rust/src/proxy/ccr.rs|rust/src/proxy/ccr_robustness_tests.rs|rust/src/tools/ctx_expand.rs|rust/src/tools/registered/ctx_retrieve.rs|rust/src/core/read_stub_index.rs|rust/src/core/tabular_crush.rs) CCR_CHANGED=1 ;;
|
||
esac
|
||
case "$f" in
|
||
rust/tests/*|*/tests/*|*test*.rs|*tests.rs) TEST_SIGNAL=1 ;;
|
||
esac
|
||
done <<< "$files"
|
||
|
||
# Inline tests live in the same .rs file as the code (#[cfg(test)] mod tests),
|
||
# so a filename check misses them. If no test *file* changed, look for test
|
||
# signal inside the Rust diff itself.
|
||
if [[ "$TEST_SIGNAL" -eq 0 ]]; then
|
||
if git -C "$REPO_ROOT" diff "$base" -- rust 2>/dev/null \
|
||
| grep -Eq '^[+-].*(#\[test\]|#\[cfg\(test\)\]|assert(_eq|_ne)?!|proptest!)'; then
|
||
TEST_SIGNAL=1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
cd "$REPO_ROOT/rust"
|
||
|
||
# Match CI's environment. RUSTFLAGS=-Dwarnings is applied *per step* (only where
|
||
# it must change the build fingerprint — the Windows cross-check and the full
|
||
# test build) so the fast host checks keep sharing the normal dev target cache
|
||
# instead of recompiling the whole dependency tree.
|
||
export RUSTDOCFLAGS="-Dwarnings"
|
||
export LEAN_CTX_EMBEDDINGS_AUTO_DOWNLOAD=0
|
||
# Keep proptest snappy like CI (local default is 256); override if you want more.
|
||
export PROPTEST_CASES="${PROPTEST_CASES:-64}"
|
||
|
||
# Windows test compiles the GNU target. jemalloc needs MinGW (not available on
|
||
# a plain dev box), so we cross-*check* with the default feature set minus
|
||
# jemalloc — enough to exercise the same cfg/dead-code analysis that bit us.
|
||
WIN_TARGET="x86_64-pc-windows-gnu"
|
||
WIN_FEATURES="tree-sitter,embeddings,http-server,team-server,secure-update"
|
||
|
||
BOLD="\033[1m"; CYAN="\033[1;36m"; GREEN="\033[1;32m"; RED="\033[1;31m"
|
||
YELLOW="\033[1;33m"; RESET="\033[0m"
|
||
|
||
PASSED=()
|
||
FAILED=()
|
||
SKIPPED=()
|
||
WARNED=()
|
||
|
||
step() { # step "Label" cmd...
|
||
local label="$1"; shift
|
||
printf "\n${CYAN}▶ %s${RESET}\n" "$label"
|
||
printf "${BOLD} \$ %s${RESET}\n" "$*"
|
||
if "$@"; then
|
||
PASSED+=("$label")
|
||
else
|
||
FAILED+=("$label")
|
||
fi
|
||
}
|
||
|
||
skip() { # skip "Label" "reason"
|
||
printf "\n${YELLOW}⊘ %s — skipped: %s${RESET}\n" "$1" "$2"
|
||
SKIPPED+=("$1: $2")
|
||
}
|
||
|
||
# ── Classify + decide what to run ─────────────────────────────────────
|
||
classify_changes
|
||
|
||
FORCE_ALL=0
|
||
[[ "$LEVEL" == "full" ]] && FORCE_ALL=1
|
||
[[ "$CLASSIFY_OK" -eq 0 ]] && FORCE_ALL=1
|
||
|
||
if [[ "$FORCE_ALL" -eq 1 ]]; then
|
||
RUN_RUST=1
|
||
RUN_GEN_DOCS=1
|
||
else
|
||
RUN_RUST="$RUST_CHANGED"
|
||
RUN_GEN_DOCS=0
|
||
{ [[ "$RUST_CHANGED" -eq 1 ]] || [[ "$GENERATED_DOCS_CHANGED" -eq 1 ]]; } && RUN_GEN_DOCS=1
|
||
fi
|
||
|
||
printf "${BOLD}preflight (%s) — CI-parity gate${RESET}\n" "$LEVEL"
|
||
if [[ "$CLASSIFY_OK" -eq 1 ]]; then
|
||
printf " changed files vs %s: %s (rust=%s, generated-docs=%s, contract=%s)\n" \
|
||
"$BASE_REF" "$CHANGED_COUNT" "$RUST_CHANGED" "$GENERATED_DOCS_CHANGED" "$CONTRACT_CHANGED"
|
||
else
|
||
printf " ${YELLOW}change classification unavailable (no %s) — running full gate${RESET}\n" "$BASE_REF"
|
||
fi
|
||
|
||
# Always-on cheap gate: whitespace errors + leftover conflict markers. Checks
|
||
# the pushed range (base..HEAD) when known, else the working tree.
|
||
if [[ -n "$BASE_SHA" ]]; then
|
||
step "Whitespace / conflict markers (git diff --check)" \
|
||
git -C "$REPO_ROOT" diff --check "$BASE_SHA" HEAD
|
||
else
|
||
step "Whitespace / conflict markers (git diff --check)" \
|
||
git -C "$REPO_ROOT" diff --check
|
||
fi
|
||
|
||
if [[ "$RUN_RUST" -eq 1 ]]; then
|
||
step "Format (cargo fmt --check)" \
|
||
cargo fmt --check
|
||
|
||
step "Clippy (--all-features -D warnings)" \
|
||
cargo clippy --all-features -- -D warnings
|
||
|
||
step "Docs (rustdoc -D warnings)" \
|
||
cargo doc --no-deps --all-features
|
||
else
|
||
skip "Format / Clippy / Docs" "no Rust/Cargo files changed (docs-only)"
|
||
fi
|
||
|
||
if [[ "$RUN_GEN_DOCS" -eq 1 ]]; then
|
||
step "Generated-docs drift (gen_docs --check)" \
|
||
cargo run --quiet --example gen_docs --features dev-tools -- --check
|
||
else
|
||
skip "Generated-docs drift (gen_docs --check)" "no Rust or generated-doc files changed"
|
||
fi
|
||
|
||
if [[ "$RUN_RUST" -eq 1 ]]; then
|
||
step "Registry-snapshot drift (gen_registry --check)" \
|
||
cargo run --quiet --example gen_registry --features dev-tools -- --check
|
||
else
|
||
skip "Registry-snapshot drift (gen_registry --check)" "no Rust files changed"
|
||
fi
|
||
|
||
if [[ "$RUN_RUST" -eq 1 ]]; then
|
||
if rustup target list --installed 2>/dev/null | grep -qx "$WIN_TARGET"; then
|
||
step "Windows cross-compile ($WIN_TARGET)" \
|
||
env RUSTFLAGS=-Dwarnings cargo check --target "$WIN_TARGET" --lib --tests \
|
||
--no-default-features --features "$WIN_FEATURES"
|
||
else
|
||
skip "Windows cross-compile ($WIN_TARGET)" \
|
||
"target not installed — run: rustup target add $WIN_TARGET"
|
||
fi
|
||
else
|
||
skip "Windows cross-compile ($WIN_TARGET)" "no Rust/Cargo files changed (docs-only)"
|
||
fi
|
||
|
||
# CCR robustness gate (#983) — change-aware: `full` already runs every lib test,
|
||
# so only `fast` needs this focused build, and only when the recovery surface
|
||
# changed. A test build is otherwise kept out of `fast` by design.
|
||
if [[ "$LEVEL" != "full" && "$RUN_RUST" -eq 1 && "$CCR_CHANGED" -eq 1 ]]; then
|
||
step "CCR robustness regression (cargo test --lib ccr_robustness)" \
|
||
env RUSTFLAGS=-Dwarnings cargo test --lib --all-features ccr_robustness
|
||
elif [[ "$LEVEL" != "full" ]]; then
|
||
skip "CCR robustness regression" "no CCR recovery files changed"
|
||
fi
|
||
|
||
if [[ "$LEVEL" = "full" ]]; then
|
||
step "Unit tests (cargo test --lib)" \
|
||
env RUSTFLAGS=-Dwarnings cargo test --lib --all-features
|
||
|
||
# Entrypoint + rules-drift smoke gates (#902/#903). Integration tests, so they
|
||
# build the bin — kept out of `fast` (which deliberately avoids test builds);
|
||
# CI runs them unconditionally via `cargo test --all-features`.
|
||
step "Entrypoint + rules drift (#902/#903)" \
|
||
env RUSTFLAGS=-Dwarnings cargo test --all-features --test entrypoints_wired --test rules_drift
|
||
fi
|
||
|
||
# ── No-test policy (#849) ─────────────────────────────────────────────
|
||
# Contract code changed but the diff carries no test signal → flag it. Advisory
|
||
# unless LEAN_CTX_PREFLIGHT_STRICT_TESTS=1. Never triggers for docs/metadata-only
|
||
# changes (CONTRACT_CHANGED stays 0).
|
||
if [[ "$CLASSIFY_OK" -eq 1 && "$CONTRACT_CHANGED" -eq 1 && "$TEST_SIGNAL" -eq 0 ]]; then
|
||
MSG="contract code changed (proxy/tools/config-schema) but the diff adds no test signal — add/adjust tests or justify"
|
||
if [[ "${LEAN_CTX_PREFLIGHT_STRICT_TESTS:-0}" == "1" ]]; then
|
||
printf "\n${RED}▶ No-test policy (#849)${RESET}\n ${RED}%s${RESET}\n" "$MSG"
|
||
FAILED+=("No-test policy: $MSG")
|
||
else
|
||
printf "\n${YELLOW}▶ No-test policy (#849) — advisory${RESET}\n ${YELLOW}%s${RESET}\n" "$MSG"
|
||
printf " ${YELLOW}(set LEAN_CTX_PREFLIGHT_STRICT_TESTS=1 to make this blocking)${RESET}\n"
|
||
WARNED+=("No-test policy: $MSG")
|
||
fi
|
||
fi
|
||
|
||
# ── Summary ───────────────────────────────────────────────────────────
|
||
printf "\n${BOLD}── preflight summary ──${RESET}\n"
|
||
printf "${GREEN} ok passed: %d${RESET}\n" "${#PASSED[@]}"
|
||
if [ "${#SKIPPED[@]}" -gt 0 ]; then
|
||
printf "${YELLOW} -- skipped: %d${RESET}\n" "${#SKIPPED[@]}"
|
||
for s in "${SKIPPED[@]}"; do printf "${YELLOW} - %s${RESET}\n" "$s"; done
|
||
fi
|
||
if [ "${#WARNED[@]}" -gt 0 ]; then
|
||
printf "${YELLOW} !! warned: %d${RESET}\n" "${#WARNED[@]}"
|
||
for w in "${WARNED[@]}"; do printf "${YELLOW} - %s${RESET}\n" "$w"; done
|
||
fi
|
||
if [ "${#FAILED[@]}" -gt 0 ]; then
|
||
printf "${RED} XX failed: %d${RESET}\n" "${#FAILED[@]}"
|
||
for f in "${FAILED[@]}"; do printf "${RED} - %s${RESET}\n" "$f"; done
|
||
printf "\n${RED}preflight FAILED — fix the above before pushing.${RESET}\n"
|
||
exit 1
|
||
fi
|
||
|
||
printf "\n${GREEN}preflight PASSED — safe to push.${RESET}\n"
|