testsAndMisc/meta/scripts/ci_mirror.sh
Krzysztof Rudnicki 4d9f486176 billsplit: build a real APK by dropping file_picker for file_selector
billsplit had never produced an Android APK. file_picker 11.0.3 — the latest
release on pub.dev — is wedged under AGP 9 in both directions: it skips
applying the Kotlin Gradle Plugin when AGP >= 9, so with built-in Kotlin off
its own Kotlin never compiles (`cannot find symbol: FilePickerPlugin`), and
with built-in Kotlin on its buildscript leaks KGP onto the global classpath
via rootProject.allprojects, which AGP then rejects for every module.

The app's Gradle config was never at fault: android/app/build.gradle.kts and
settings.gradle.kts are byte-identical to habit_stack, home_inventory and
diet_guard_app, which all build. Only the dependency differed, so the fix is
the dependency — the toolchain stays on the house-standard AGP 9.

Ported both call sites to the official file_selector. Its save flow returns a
destination and leaves the write to the caller, where file_picker wrote the
bytes inside the plugin; that write now goes through an injectable seam
because a real dart:io future never completes inside testWidgets' fake-async
zone, so a widget test could otherwise never observe the resulting snackbar.
The test double moves from a MethodChannel mock to a FileSelectorPlatform fake
(file_selector speaks Pigeon, so a channel mock would not intercept it), and a
plain non-widget test covers the real on-disk writer to hold 100% coverage
without an ignore.

The launcher icon is now verified from the built artifact: the ic_launcher
assets unzipped from the APK render as the charcoal + gold perforated receipt,
not the stock Flutter beach. Peak build footprint is 1.86 GiB, inside budget.

Also rewrites ci_mirror.sh to run its gates in a disposable worktree at HEAD.
`pre-commit run --all-files` stashes the live tree, and in a shared checkout
that made an unrelated agent's uncommitted work collide with hook auto-fixes,
failing the push while blaming an innocent hook — and putting that work inside
a stash window, which memories/git-rules.md forbids. A clean worktree needs no
stash and is a truer mirror of CI, which never sees a dirty file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:15:40 +02:00

125 lines
4.5 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# ci_mirror.sh — run the CI gates locally before a push can leave (monorepo).
#
# testsAndMisc variant of the repo-wide CI-mirror gate. Same idea as the
# single-package version: the installed pre-commit hook only checks staged
# files and the dev env already has every dependency, whereas CI runs
# `pre-commit run --all-files` and installs a fresh env from requirements.txt.
#
# Differences from the single-package version:
# * requirements live at the repo root requirements.txt (what CI installs);
# * tests run through meta/scripts/pytest_changed_packages.py rather than a
# raw `pytest --cov=python_pkg` — this is the OOM-safe runner this machine
# relies on (no xdist, bounded memory), chosen deliberately over the full
# monorepo suite. It invokes pytest via sys.executable, so running it with
# the clean venv's python routes coverage through the venv.
#
# Wired as the pre-push hook; a red result blocks the push before CI sees it.
# Escape hatch: `git push --no-verify`.
# ============================================================================
set -euo pipefail
REQUIREMENTS_FILE="${REQUIREMENTS_FILE:-requirements.txt}"
readonly REQUIREMENTS_FILE
ROOT="$(git rev-parse --show-toplevel)"
readonly ROOT
cd "$ROOT"
readonly VENV_DIR="$ROOT/.ci-mirror-venv"
readonly HASH_FILE="$VENV_DIR/.requirements.sha256"
readonly REQ_PATH="$ROOT/$REQUIREMENTS_FILE"
log() { printf 'ci-mirror: %s\n' "$1" >&2; }
fail() {
log "FAILED — $1"
log "CI would be red. Fix the above, or 'git push --no-verify' to override."
exit 1
}
require_file() {
if [[ ! -f "$REQ_PATH" ]]; then
fail "requirements file not found: $REQ_PATH"
fi
}
# Rebuild the venv only when requirements.txt changed since the last build.
ensure_venv() {
local current stored
current="$(sha256sum "$REQ_PATH" | cut -d' ' -f1)"
stored=""
if [[ -f "$HASH_FILE" ]]; then
stored="$(cat "$HASH_FILE")"
fi
if [[ -x "$VENV_DIR/bin/python" && "$current" == "$stored" ]]; then
log "venv up to date (requirements.txt unchanged)"
return
fi
log "requirements.txt changed — rebuilding clean venv (mirrors CI install)"
rm -rf "$VENV_DIR"
python3 -m venv "$VENV_DIR"
"$VENV_DIR/bin/python" -m pip install --quiet --upgrade pip ||
fail "pip self-upgrade in the clean venv"
"$VENV_DIR/bin/python" -m pip install --quiet -r "$REQ_PATH" ||
fail "pip install -r $REQUIREMENTS_FILE (a dep may be undeclared)"
printf '%s' "$current" >"$HASH_FILE"
log "clean venv ready"
}
# Disposable clean checkout of HEAD; see run_gates_in_clean_worktree.
CHECKOUT=""
cleanup_checkout() {
if [[ -n "$CHECKOUT" && -d "$CHECKOUT" ]]; then
git -C "$ROOT" worktree remove --force "$CHECKOUT" >/dev/null 2>&1 || true
rm -rf "$CHECKOUT"
git -C "$ROOT" worktree prune >/dev/null 2>&1 || true
fi
}
trap cleanup_checkout EXIT
# Run the gates against a throwaway worktree at HEAD rather than the live tree.
#
# `pre-commit run --all-files` stashes unstaged changes, runs the hooks, then
# restores. In a shared checkout that is actively hostile: when another agent
# (or the user) has uncommitted work that collides with a hook's auto-fix, the
# restore conflicts, pre-commit rolls the fixes back, and the push fails with a
# diagnostic that points at the innocent hook rather than at the collision.
# Worse, the stash window puts someone else's uncommitted work at risk, which
# is exactly what memories/git-rules.md forbids.
#
# A clean worktree removes the stash entirely and is a truer mirror of CI,
# which always builds from a pristine checkout and never sees a dirty file.
run_gates_in_clean_worktree() {
CHECKOUT="$(mktemp -d -t ci-mirror-XXXXXX)"
rm -rf "$CHECKOUT"
log "checking out HEAD into a clean worktree (no stash of your tree)"
git -C "$ROOT" worktree add --detach --quiet "$CHECKOUT" HEAD ||
fail "could not create the clean worktree"
# Skip pytest-coverage here: it is a pre-commit-stage hook that would run
# in the system env (not our clean venv) and duplicate the pytest run below.
log "pre-commit run --all-files (mirrors the pre-commit workflow)"
(cd "$CHECKOUT" && SKIP=pytest-coverage pre-commit run --all-files) ||
fail "pre-commit --all-files"
log "changed-packages pytest in the clean venv (OOM-safe runner)"
(cd "$CHECKOUT" && "$VENV_DIR/bin/python" meta/scripts/pytest_changed_packages.py) ||
fail "pytest_changed_packages (clean requirements.txt venv)"
}
main() {
require_file
ensure_venv
run_gates_in_clean_worktree
log "all CI gates passed locally — safe to push"
}
main "$@"