diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84a3e5735..265ee7aea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,21 +172,11 @@ jobs: # (shared effect application) is complete. # See: .progress/012_20260217_dst_architecture_fix.md + - name: Dependency isolation regression + run: bash scripts/test-dependency-isolation.sh + - name: Dependency isolation - run: | - # Check temper-jit has no production dep on temper-verify - if cargo tree --no-dev -p temper-jit 2>/dev/null | grep -q temper-verify; then - echo "FAIL: temper-jit has production dependency on temper-verify" - exit 1 - fi - # Check no stateright/proptest in production crates - for crate in temper-jit temper-server temper-runtime; do - if cargo tree --no-dev -p "$crate" 2>/dev/null | grep -qE 'stateright|proptest'; then - echo "FAIL: $crate production binary includes stateright or proptest" - exit 1 - fi - done - echo "Dependency isolation: OK" + run: bash scripts/check-dependency-isolation.sh # ───────────────────────────────────────────────────── # Gate 3: Full test suite (minutes) diff --git a/scripts/check-dependency-isolation.sh b/scripts/check-dependency-isolation.sh new file mode 100755 index 000000000..fa0017bd7 --- /dev/null +++ b/scripts/check-dependency-isolation.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Check default-feature, non-dev dependency graphs used by the CI isolation gate. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +for crate in temper-jit temper-server temper-runtime; do + # Buffer the complete graph and check Cargo's exit before inspecting it. + # A grep -q pipeline can hide query errors or cause a producer SIGPIPE. + if graph="$(cargo tree --edges no-dev -p "$crate" --color never)"; then + if [[ ! "$graph" =~ [^[:space:]] ]]; then + echo "FAIL: cargo tree returned an empty dependency graph for $crate" >&2 + exit 1 + fi + else + status=$? + echo "FAIL: cargo tree --edges no-dev -p $crate failed (exit $status)" >&2 + exit "$status" + fi + + if [[ "$crate" == temper-jit && "$graph" =~ (^|[[:space:]])temper-verify[[:space:]] ]]; then + echo "FAIL: temper-jit has production dependency on temper-verify" >&2 + exit 1 + fi + if [[ "$graph" =~ (^|[[:space:]])(stateright|proptest)[[:space:]] ]]; then + echo "FAIL: $crate production binary includes stateright or proptest" >&2 + exit 1 + fi +done + +echo "Dependency isolation: OK" diff --git a/scripts/test-dependency-isolation.sh b/scripts/test-dependency-isolation.sh new file mode 100755 index 000000000..5d083288a --- /dev/null +++ b/scripts/test-dependency-isolation.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# Exercise the same guard CI invokes, with controlled Cargo output and exits. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +GUARD="${1:-$ROOT/scripts/check-dependency-isolation.sh}" +TASK_TMP="$(mktemp -d "${TMPDIR:-/tmp}/temper-dependency-test.XXXXXX")" +trap 'rm -rf "$TASK_TMP"' EXIT +mkdir "$TASK_TMP/bin" + +cat > "$TASK_TMP/bin/cargo" <<'MOCK' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$CALLS" +# The old CI query is accepted here so the RED detects its swallowed failure, +# rather than merely testing an argument mismatch. +crate='' +while [[ $# -gt 0 ]]; do + if [[ "$1" == '-p' ]]; then + crate="$2" + break + fi + shift +done +if [[ "$crate" == "$TARGET_CRATE" ]]; then + case "$MODE" in + cargo-error) + printf '%s v0.1.0\n' "$crate" + echo 'cargo fixture: dependency query failed' >&2 + exit 42 + ;; + sigpipe) + printf '%s v0.1.0\n' "$crate" + echo 'cargo fixture: interrupted output' >&2 + exit 141 + ;; + empty) exit 0 ;; + whitespace) printf ' \t\n'; exit 0 ;; + esac +fi +printf '%s v0.1.0\n' "$crate" +if [[ "$crate" == "$TARGET_CRATE" ]]; then + case "$MODE" in + forbidden|large-forbidden) + printf '├── %s v1.0.0\n' "$FORBIDDEN" + ;; + allowed) + printf '├── proptest-helper v1.0.0\n└── stateright-helper v1.0.0\n' + ;; + esac + if [[ "$MODE" == 'large-forbidden' ]]; then + for ((i = 0; i < 10000; i++)); do + printf '├── allowed-dependency-%s v1.0.0\n' "$i" + done + fi +fi +MOCK +chmod +x "$TASK_TMP/bin/cargo" + +check_case() { + local name="$1" mode="$2" crate="$3" forbidden="$4" expected="$5" diagnostic="$6" + local actual=0 + local output="$TASK_TMP/$name.stdout" errors="$TASK_TMP/$name.stderr" + local calls="$TASK_TMP/$name.calls" + if PATH="$TASK_TMP/bin:$PATH" MODE="$mode" TARGET_CRATE="$crate" \ + FORBIDDEN="$forbidden" CALLS="$calls" bash "$GUARD" > "$output" 2> "$errors"; then + actual=0 + else + actual=$? + fi + if [[ "$actual" -ne "$expected" ]]; then + cat "$output" "$errors" >&2 + printf 'FAIL %s: expected exit %s, got %s\n' "$name" "$expected" "$actual" >&2 + exit 1 + fi + if [[ "$expected" -eq 0 ]]; then + grep -Fx 'Dependency isolation: OK' "$output" + printf 'tree --edges no-dev -p %s --color never\n' \ + temper-jit temper-server temper-runtime > "$TASK_TMP/expected.calls" + diff -u "$TASK_TMP/expected.calls" "$calls" + else + if grep -F 'Dependency isolation: OK' "$output" "$errors"; then + printf 'FAIL %s: failure reported success\n' "$name" >&2 + exit 1 + fi + grep -F "$diagnostic" "$errors" + fi + printf 'PASS %s (exit %s)\n' "$name" "$actual" +} + +# First case is the discriminating regression for the original if-pipeline. +check_case cargo-error-jit cargo-error temper-jit '' 42 'cargo fixture: dependency query failed' +check_case allowed allowed temper-jit '' 0 '' +check_case jit-verifier forbidden temper-jit temper-verify 1 'temper-jit has production dependency on temper-verify' +# temper-verify itself is only forbidden in temper-jit, not the other roots. +for crate in temper-server temper-runtime; do + check_case "$crate-verifier-allowed" forbidden "$crate" temper-verify 0 '' +done +for crate in temper-jit temper-server temper-runtime; do + for forbidden in stateright proptest; do + check_case "$crate-$forbidden" forbidden "$crate" "$forbidden" 1 "$crate production binary includes stateright or proptest" + done + check_case "$crate-empty" empty "$crate" '' 1 "cargo tree returned an empty dependency graph for $crate" +done +for crate in temper-server temper-runtime; do + check_case "$crate-cargo-error" cargo-error "$crate" '' 42 'cargo fixture: dependency query failed' +done +check_case whitespace whitespace temper-jit '' 1 'cargo tree returned an empty dependency graph for temper-jit' +check_case sigpipe sigpipe temper-jit '' 141 'cargo fixture: interrupted output' +check_case large-forbidden large-forbidden temper-jit proptest 1 'temper-jit production binary includes stateright or proptest'