004479a032
`scripts/ci.sh` is the source of truth for what must pass before a PR is mergeable. CLAUDE.md section 10.4 requires the Gitea workflow to run the same checks; the workflow calls this script rather than reimplementing the steps, so the two cannot drift apart. Runs: cargo fmt --check, clippy -D warnings, cargo test, the section 5 size gate, Android lint/assembleRelease, and a gitleaks scan. Verified behaviour, rather than assumed: - **Failures really fail.** Injected a syntax error into `core/src/lib.rs` and confirmed `ci.sh --keep-going` exits 1 and names all four dependent checks; then restored the file and confirmed a clean exit 0. `--keep-going` captures failures per step in a subshell instead of letting `set -e` abort the run, so the summary records what already passed. - **Skips are never passes.** Android and gitleaks both skip on this machine (no SDK, no gitleaks), and each SKIP names what was skipped and why, with the summary listing them separately from the passes. A skip that reads as a green tick is how CI quietly stops testing anything. - **Runs from any CWD.** `cargo` resolves a workspace from the CWD, not from the script's location, so `ci.sh` cds to the repo root. Verified by running it from `/tmp`. - **`cargo` is on PATH.** rustup adds `~/.cargo/bin` via `~/.bashrc`, which a non-interactive shell does not read — so a script invoked from CI or by absolute path failed with "command not found" until `lib.sh` prepends it. - **Size gate resolves absolute paths.** `CARGO_TARGET_DIR` is absolute by default, so joining it onto the repo root produced a doubled path; the default is now only joined when relative. Build artifacts go to `$HOME/.blocker-target`, outside the repo, so `target/` never appears in `git status` and can be deleted in one command. Co-Authored-By: Claude Code <noreply@anthropic.com>
193 lines
6.6 KiB
Bash
Executable File
193 lines
6.6 KiB
Bash
Executable File
#!/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}" |