#!/usr/bin/env bash # # Blocker CI — the source of truth for what must pass before a PR is mergeable. # # CLAUDE.md section 10.4 requires a Gitea workflow to run these same checks. It # calls THIS script rather than reimplementing the steps, so the two cannot # drift apart. That is the whole reason this file exists separately from any # workflow definition. # # Usage: # scripts/ci.sh run every check, stop at the first failure # scripts/ci.sh --keep-going run every check, summarise all failures at the end # scripts/ci.sh --help # # Note on skips: a skipped check never counts as a pass. Every skip names what # was skipped and why, because a skip that reads as a green tick is how CI ends # up quietly not testing anything. set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/lib.sh" KEEP_GOING=0 usage() { cat <<'USAGE' Usage: scripts/ci.sh [--keep-going] [--help] Checks, in order: 1. cargo fmt --all --check 2. cargo clippy --workspace --all-targets -- -D warnings 3. cargo test --workspace --all-targets 4. size gate: stripped release daemon <= 5 MB (section 5) 5. Android lint + assembleRelease (skipped loudly if no SDK) 6. gitleaks secret scan (skipped loudly if not installed) --keep-going run every check and report all failures together USAGE } while (( $# )); do case "$1" in --keep-going) KEEP_GOING=1 ;; -h | --help) usage exit 0 ;; *) printf 'unknown argument: %s\n' "$1" >&2 usage >&2 exit 2 ;; esac shift done TOTAL_STEPS=6 FAILED=() SKIPPED=() STEP_NUM=0 run_step() { STEP_NUM=$((STEP_NUM + 1)) printf '\n%s[%d/%d]%s %s\n' \ "${C_BLUE}" "${STEP_NUM}" "${TOTAL_STEPS}" "${C_RESET}" "$1" shift local output # Each check runs in a subshell so a failure is capturable and the # --keep-going summary can report it, rather than `set -e` aborting the # whole run with no record of what had already passed. if output="$("$@" 2>&1)"; then [[ -n "${output}" ]] && printf '%s\n' "${output}" | sed 's/^/ /' return 0 fi [[ -n "${output}" ]] && printf '%s\n' "${output}" | sed 's/^/ /' return 1 } record() { if (( KEEP_GOING )); then FAILED+=("$1") return 0 fi printf '\n%scheck failed:%s %s\n' "${C_RED}" "${C_RESET}" "$1" exit 1 } printf '%sBlocker CI%s\n' "${C_BLUE}" "${C_RESET}" info "repo: ${REPO_ROOT}" info "target: ${CARGO_TARGET_DIR}" # Cargo resolves a workspace from the current directory, not from the script's # location, so a human running this from ~/Desktop and a CI runner invoking it by # absolute path both need the CWD moved. Everything below can then use plain # `cargo` invocations. cd -- "${REPO_ROOT}" # --- 1. Formatting --------------------------------------------------------- if run_step 'cargo fmt --all --check' cargo fmt --all -- --check; then pass 'formatting' else record 'cargo fmt --all --check' fi # --- 2. Clippy ------------------------------------------------------------- if run_step 'cargo clippy -D warnings' \ cargo clippy --workspace --all-targets -- -D warnings; then pass 'clippy' else record 'cargo clippy -D warnings' fi # --- 3. Tests -------------------------------------------------------------- if run_step 'cargo test --workspace' cargo test --workspace --all-targets; then pass 'tests (including golden decision tables)' else record 'cargo test --workspace' fi # --- 4. Size gate ---------------------------------------------------------- # Section 5: the stripped desktop daemon must not exceed 5 MB. In M0 the daemon # is a placeholder that prints two lines, so this is far under budget; the gate # becomes meaningful in M2-Linux once enforcement code exists. It runs from M0 # anyway so that the budget is enforced by tooling from the first commit rather # than being introduced as a new failure later. printf '\n%s[4/6]%s size gate (section 5: stripped daemon <= 5 MB)\n' \ "${C_BLUE}" "${C_RESET}" if run_step 'cargo build --release -p blocker-daemon' \ cargo build --release -p blocker-daemon; then if "${REPO_ROOT}/scripts/size-check.sh"; then pass 'size gate' else record 'size gate' fi else record 'cargo build --release -p blocker-daemon' fi # --- 5. Android ------------------------------------------------------------ # Skipped loudly, never silently. CLAUDE.md section 10.4 says to ask whether the # runner has the SDK; until that is answered, detection plus an honest message is # the truthful behaviour. A pass here would be a lie. printf '\n%s[5/6]%s Android lint + assembleRelease\n' "${C_BLUE}" "${C_RESET}" if SDK_ROOT="$(android_sdk_root)"; then info "SDK: ${SDK_ROOT}" if run_step 'android assembleRelease' \ "${REPO_ROOT}/android/gradlew" -p "${REPO_ROOT}/android" lint assembleRelease; then pass 'android' else record 'android lint/assembleRelease' fi else skip 'android lint + assembleRelease' \ 'no Android SDK found (ANDROID_HOME / ANDROID_SDK_ROOT unset, ~/Android/Sdk absent). Not verified by this run.' SKIPPED+=('android') fi # --- 6. Secret scan -------------------------------------------------------- # Section 10.1 requires a secret scanner in CI and in ci.sh. gitleaks is not # installed on every machine and that is not a reason to skip it quietly. printf '\n%s[6/6]%s secret scan (gitleaks)\n' "${C_BLUE}" "${C_RESET}" if command -v gitleaks > /dev/null 2>&1; then # `gitleaks detect` never prints matched secret values, only file paths and # rule names — so this output is safe to paste into a PR. if run_step 'gitleaks detect' \ gitleaks detect --source "${REPO_ROOT}" --redact --no-banner; then pass 'secret scan' else record 'gitleaks detect' fi else skip 'gitleaks secret scan' \ 'gitleaks not installed. Install it (https://github.com/gitleaks/gitleaks) before merging. NOT a pass.' SKIPPED+=('gitleaks') fi # --- Summary --------------------------------------------------------------- printf '\n%s%s%s\n' "${C_BLUE}" '── summary ──' "${C_RESET}" if (( ${#FAILED[@]} )); then printf '%s%d check(s) failed:%s\n' "${C_RED}" "${#FAILED[@]}" "${C_RESET}" for f in "${FAILED[@]}"; do printf ' - %s\n' "${f}"; done exit 1 fi if (( ${#SKIPPED[@]} )); then printf '%s%d check(s) skipped — these are NOT passes:%s\n' \ "${C_YELLOW}" "${#SKIPPED[@]}" "${C_RESET}" for s in "${SKIPPED[@]}"; do printf ' - %s\n' "${s}"; done fi printf '%sall executed checks passed%s\n' "${C_GREEN}" "${C_RESET}"