Files
blocker/scripts/lib.sh
T
SellPL 004479a032 ci: add ci.sh, size gate, and shared script helpers
`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>
2026-10-01 15:16:58 +03:00

108 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Shared helpers for the Blocker CI scripts.
#
# Sourced, never executed.
set -euo pipefail
# --- Colours ---------------------------------------------------------------
# Only when stdout is a terminal, so CI logs stay clean and diffable.
if [[ -t 1 ]]; then
C_RESET=$'\033[0m'
C_RED=$'\033[31m'
C_GREEN=$'\033[32m'
C_YELLOW=$'\033[33m'
C_BLUE=$'\033[34m'
C_DIM=$'\033[2m'
else
C_RESET='' C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_DIM=''
fi
# --- Paths -----------------------------------------------------------------
# Resolve the repo root from this file's location so every script works from
# any working directory. CI invokes these from the workspace root; humans
# invoke them from wherever they happen to be.
LIB_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${LIB_DIR}/.." && pwd)"
# --- Build output ----------------------------------------------------------
# Keep build artifacts OUT of the repo (section 10.1: build outputs are not in
# git) and in one deletable place. Overridable, but the default matters: a
# target/ inside the repo shows up in `git status` and tempts a `git add -A`.
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-${HOME}/.blocker-target}"
# --- Toolchain on PATH -----------------------------------------------------
# rustup installs shims into ~/.cargo/bin and adds a line to ~/.bashrc and
# ~/.profile. A non-interactive shell does not read ~/.bashrc, so a script run
# from CI, a cron job, or a different CWD can fail with "cargo: command not
# found" even on a correctly installed machine. Adding the directory here is
# idempotent and does not modify the user's shell config.
if [[ -d "${HOME}/.cargo/bin" ]]; then
case ":${PATH}:" in
*":${HOME}/.cargo/bin:"*) ;;
*) PATH="${HOME}/.cargo/bin:${PATH}" ;;
esac
export PATH
fi
# --- Section 5 budgets -----------------------------------------------------
MAX_DAEMON_BYTES=$((5 * 1024 * 1024)) # 5 MB stripped daemon
# --- Reporting -------------------------------------------------------------
_step=0
step() {
_step=$((_step + 1))
printf '\n%s[%d/%d]%s %s\n' "${C_BLUE}" "${_step}" "${TOTAL_STEPS:-?}" "${C_RESET}" "$1"
}
pass() {
printf ' %sPASS%s %s\n' "${C_GREEN}" "${C_RESET}" "$1"
}
fail() {
printf ' %sFAIL%s %s\n' "${C_RED}" "${C_RESET}" "$1"
}
# A skip that reads as a pass is the classic CI lie (CLAUDE.md section 10.4).
# Skips must always name what was skipped and why.
skip() {
printf ' %sSKIP%s %s\n' "${C_YELLOW}" "${C_RESET}" "$1"
printf ' %sreason:%s %s\n' "${C_DIM}" "${C_RESET}" "$2"
}
info() {
printf ' %s%s%s\n' "${C_DIM}" "$1" "${C_RESET}"
}
# Announce a non-fatal environment note once, before the checks run, so a
# reader knows what to expect rather than wondering why a stage vanished.
announce_skip() {
printf '%s%s%s\n' "${C_YELLOW}" "SKIP: $1 — $2" "${C_RESET}"
}
die() {
printf '\n%sfatal:%s %s\n' "${C_RED}" "${C_RESET}" "$1" >&2
exit 1
}
# --- Android SDK detection -------------------------------------------------
# CLAUDE.md section 10.4 asks whether the runner has the SDK. Rather than assume
# either way, detect it and report honestly.
android_sdk_root() {
if [[ -n "${ANDROID_HOME:-}" && -d "${ANDROID_HOME}" ]]; then
printf '%s' "${ANDROID_HOME}"
return 0
fi
if [[ -n "${ANDROID_SDK_ROOT:-}" && -d "${ANDROID_SDK_ROOT}" ]]; then
printf '%s' "${ANDROID_SDK_ROOT}"
return 0
fi
# Common default install location.
if [[ -d "${HOME}/Android/Sdk" ]]; then
printf '%s' "${HOME}/Android/Sdk"
return 0
fi
return 1
}