mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 20:27:11 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWXTUM9dpnCS2wtFkx4
198 lines
7.6 KiB
Bash
Executable File
198 lines
7.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# ============================================================================
|
|
# ci_mirror.sh — run the exact CI gates locally before a push can leave.
|
|
#
|
|
# Why: the installed pre-commit hook only checks *staged* files, and the local
|
|
# dev environment already has every dependency installed. CI does neither — it
|
|
# runs `pre-commit run --all-files` and installs a fresh environment from
|
|
# requirements.txt. That gap is why green-locally repos went red in CI
|
|
# (missing runtime deps, --all-files lint debt, order-dependent test flakes).
|
|
#
|
|
# This script reproduces both CI workflows on the developer machine:
|
|
# 1. a venv built ONLY from requirements.txt (mirrors the Tests workflow's
|
|
# `pip install -r requirements.txt`), rebuilt only when requirements.txt
|
|
# changes (hash-gated so day-to-day pushes stay fast);
|
|
# 2. `pre-commit run --all-files` (mirrors the pre-commit workflow);
|
|
# 3. `python -m pytest` inside that clean venv (mirrors Tests);
|
|
# 4. `flutter analyze` + `flutter test` for the companion app in app/.
|
|
#
|
|
# The app is gated here rather than only by pre-commit because pre-commit has
|
|
# no Dart hooks at all: without this, the app's tests were never run by
|
|
# anything automatic, and a red app/ could be pushed with a green gate.
|
|
#
|
|
# Runs on demand (`scripts/check_gate_stamp.sh full`, pre-commit's manual
|
|
# stage) and in CI. It left the pre-push path on 2026-09-20: 10+ minutes
|
|
# per push under the resource cap, for work finish_auto.sh had just done.
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
# Requirements file may be overridden (e.g. testsAndMisc uses meta/…).
|
|
REQUIREMENTS_FILE="${REQUIREMENTS_FILE:-requirements.txt}"
|
|
readonly REQUIREMENTS_FILE
|
|
|
|
ROOT="$(git rev-parse --show-toplevel)"
|
|
readonly ROOT
|
|
cd "$ROOT"
|
|
|
|
readonly APP_DIR="$ROOT/app"
|
|
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 before pushing."
|
|
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"
|
|
}
|
|
|
|
run_precommit_all_files() {
|
|
log "pre-commit run --all-files (mirrors the pre-commit workflow)"
|
|
pre-commit run --all-files || fail "pre-commit --all-files"
|
|
}
|
|
|
|
run_pytest_clean_venv() {
|
|
log "pytest in the clean venv (mirrors the Tests workflow)"
|
|
# Under xvfb-run, exactly as python-tests.yml does. The gate's real-Tk
|
|
# tests walk a focus ring via tk_focusNext, which consults mapped-ness
|
|
# and real focus: on a developer's live display the window never takes
|
|
# focus and the ring collapses to one widget, so TestRealFocusRing fails
|
|
# here while passing in CI. Mirroring the workflow's display is the whole
|
|
# point of this script -- without it the mirror reports red on a green
|
|
# tree, which is worse than not mirroring at all.
|
|
if ! command -v xvfb-run >/dev/null 2>&1; then
|
|
fail "xvfb-run is not on PATH (install xorg-server-xvfb); the real-Tk
|
|
gate tests need the same virtual display the Tests workflow gives them"
|
|
fi
|
|
xvfb-run -a -s "-screen 0 1600x1200x24" \
|
|
"$VENV_DIR/bin/python" -m pytest "$@" \
|
|
|| fail "pytest (clean requirements.txt venv)"
|
|
}
|
|
|
|
# Run flutter with the calling git hook's environment stripped.
|
|
#
|
|
# `git push` exports GIT_DIR (and friends) to its hooks, and the flutter tool
|
|
# shells out to git to identify its own SDK. With GIT_DIR set it reads THIS
|
|
# repository instead of /opt/flutter: `flutter --version` then reports our
|
|
# HEAD as the framework revision and our URL as the repository, pub resolves
|
|
# the SDK as `0.0.0-unknown`, every version constraint fails, and the wrong
|
|
# answer is written into flutter's own version cache for the next caller.
|
|
flutter_clean_env() {
|
|
env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX \
|
|
-u GIT_COMMON_DIR -u GIT_OBJECT_DIRECTORY flutter "$@"
|
|
}
|
|
|
|
# Gate the Flutter companion app the same way: analyze (pre-commit has no Dart
|
|
# hooks) then the full test suite. Deliberately fails rather than skips when
|
|
# flutter is missing -- a gate that quietly passes on a machine without the
|
|
# toolchain is the same as no gate.
|
|
require_app() {
|
|
if [[ ! -f "$APP_DIR/pubspec.yaml" ]]; then
|
|
log "no app/pubspec.yaml — skipping the Flutter gates"
|
|
return 1
|
|
fi
|
|
if ! command -v flutter >/dev/null 2>&1; then
|
|
fail "flutter is not on PATH but app/ exists (cannot verify the app)"
|
|
fi
|
|
}
|
|
|
|
run_flutter_analyze() {
|
|
require_app || return 0
|
|
log "flutter analyze (app/)"
|
|
(cd "$APP_DIR" && flutter_clean_env analyze) || fail "flutter analyze"
|
|
}
|
|
|
|
run_flutter_test() {
|
|
require_app || return 0
|
|
log "flutter test (app/)"
|
|
(cd "$APP_DIR" && flutter_clean_env test) || fail "flutter test"
|
|
}
|
|
|
|
run_flutter_gates() {
|
|
run_flutter_analyze
|
|
run_flutter_test
|
|
}
|
|
|
|
# Bucket I of N of the app's test files (every N-th, sorted), so the N buckets
|
|
# partition the suite exactly. Each bucket stays under the 60 s action cap.
|
|
run_flutter_test_shard() {
|
|
[[ ${1:-} =~ ^([0-9]+)/([1-9][0-9]*)$ ]] || fail "flutter-test --shard wants I/N, got '${1:-}'"
|
|
local index=${BASH_REMATCH[1]} total=${BASH_REMATCH[2]} hooks i=0 file
|
|
local -a files=()
|
|
hooks="$(grep -c '^ - id: ci-mirror-flutter-test-[0-9]' "$ROOT/.pre-commit-config.yaml" || true)"
|
|
((total == hooks && index < total)) \
|
|
|| fail "shard $index/$total does not match the $hooks ci-mirror-flutter-test-N hooks"
|
|
require_app || return 0
|
|
while IFS= read -r file; do
|
|
if ((i % total == index)); then
|
|
files+=("${file#"$APP_DIR"/}")
|
|
fi
|
|
i=$((i + 1))
|
|
done < <(find "$APP_DIR/test" -name '*_test.dart' | LC_ALL=C sort)
|
|
((${#files[@]} > 0)) || fail "flutter test shard $index/$total has no test files"
|
|
log "flutter test shard $index/$total (${#files[@]} files)"
|
|
(cd "$APP_DIR" && flutter_clean_env test "${files[@]}") || fail "flutter test shard $index/$total"
|
|
}
|
|
|
|
# No phase argument (or pytest options) runs everything, exactly as CI does.
|
|
main() {
|
|
require_file
|
|
case "${1:-all}" in
|
|
venv) ensure_venv ;;
|
|
precommit) run_precommit_all_files ;;
|
|
pytest)
|
|
shift
|
|
ensure_venv
|
|
run_pytest_clean_venv "$@"
|
|
;;
|
|
flutter-analyze) run_flutter_analyze ;;
|
|
flutter-test)
|
|
[[ "${2:-}" == "--shard" ]] || fail "usage: ci_mirror.sh flutter-test --shard I/N"
|
|
run_flutter_test_shard "${3:-}"
|
|
;;
|
|
*)
|
|
[[ "${1:-}" == "all" ]] && shift
|
|
ensure_venv
|
|
run_precommit_all_files
|
|
run_pytest_clean_venv "$@"
|
|
run_flutter_gates
|
|
;;
|
|
esac
|
|
log "gates passed locally — safe to push"
|
|
}
|
|
|
|
main "$@"
|