diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9febe5f..33c1533 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -132,9 +132,82 @@ repos: - repo: local hooks: - - id: ci-mirror - name: CI mirror (clean-venv pytest + pre-commit --all-files) - entry: bash scripts/ci_mirror.sh + # Split into <60 s steps (run in this order); no-arg ci_mirror.sh is the CI form. + - id: ci-mirror-venv + name: CI mirror 1 (clean venv) + entry: bash scripts/ci_mirror.sh venv + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [manual] + - id: ci-mirror-precommit + name: CI mirror 2 (pre-commit --all-files) + entry: bash scripts/ci_mirror.sh precommit + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [manual] + - id: ci-mirror-pytest + name: CI mirror 3 (pytest, xvfb, 100% cov) + entry: bash scripts/ci_mirror.sh pytest + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [manual] + - id: ci-mirror-flutter-analyze + name: CI mirror 4 (flutter analyze) + entry: bash scripts/ci_mirror.sh flutter-analyze + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [manual] + - id: ci-mirror-flutter-test-1 + name: CI mirror 5 (flutter test 1/6) + entry: bash scripts/ci_mirror.sh flutter-test --shard 0/6 + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [manual] + - id: ci-mirror-flutter-test-2 + name: CI mirror 6 (flutter test 2/6) + entry: bash scripts/ci_mirror.sh flutter-test --shard 1/6 + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [manual] + - id: ci-mirror-flutter-test-3 + name: CI mirror 7 (flutter test 3/6) + entry: bash scripts/ci_mirror.sh flutter-test --shard 2/6 + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [manual] + - id: ci-mirror-flutter-test-4 + name: CI mirror 8 (flutter test 4/6) + entry: bash scripts/ci_mirror.sh flutter-test --shard 3/6 + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [manual] + - id: ci-mirror-flutter-test-5 + name: CI mirror 9 (flutter test 5/6) + entry: bash scripts/ci_mirror.sh flutter-test --shard 4/6 + language: system + pass_filenames: false + always_run: true + require_serial: true + stages: [manual] + - id: ci-mirror-flutter-test-6 + name: CI mirror 10 (flutter test 6/6) + entry: bash scripts/ci_mirror.sh flutter-test --shard 5/6 language: system pass_filenames: false always_run: true diff --git a/app/pubspec.yaml b/app/pubspec.yaml index ccba945..fbabe08 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -2,7 +2,7 @@ name: diet_guard_app description: "Companion phone app for diet_guard: log meals on the go." publish_to: 'none' -version: 1.0.125+1 +version: 1.0.126+1 environment: sdk: ^3.12.2 diff --git a/scripts/ci_mirror.sh b/scripts/ci_mirror.sh index 675a085..aaa7c8a 100755 --- a/scripts/ci_mirror.sh +++ b/scripts/ci_mirror.sh @@ -119,27 +119,79 @@ flutter_clean_env() { # 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. -run_flutter_gates() { +require_app() { if [[ ! -f "$APP_DIR/pubspec.yaml" ]]; then log "no app/pubspec.yaml — skipping the Flutter gates" - return + 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 - ensure_venv - run_precommit_all_files - run_pytest_clean_venv "$@" - run_flutter_gates - log "all CI gates passed locally — safe to push" + 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 "$@"