From 5f672a505ac5470042e45d361c1714302785e073 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Sun, 4 Oct 2026 16:55:43 +0200 Subject: [PATCH 1/3] Shard CI's tests by file, not by test `flutter test --total-shards` splits the tests, not the files: every shard still compiled all ~105 test files, which is most of the suite's time, and then ran a third of them, so each shard took ~3 min of the old job's 4. Each shard now takes every third file and compiles only those. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51e3479..29d840d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,11 @@ jobs: run: git ls-files -z -- 'lib/*.dart' 'test/*.dart' | xargs -0 dart format --output=none --set-exit-if-changed # The app's tests, in shards: the suite was ~4 min on its own in one job. + # + # Sharded by file, not with `flutter test --total-shards`. That splits the + # *tests*: every shard still compiled all ~105 files, which is most of the + # time, and then ran a third of them — each shard took ~3 min of the old + # job's 4. Here each shard gets every third file and compiles only those. test: name: Test (${{ matrix.shard }}/3) runs-on: ubuntu-latest @@ -107,10 +112,15 @@ jobs: - name: Run tests env: COVERAGE: ${{ github.event_name == 'push' && '--coverage' || '' }} + SHARD: ${{ matrix.shard }} run: | + set -euo pipefail + # Every third file, in a stable order. A file added tomorrow joins a + # shard on its own. + mapfile -t files < <(git ls-files -- 'test/*_test.dart' | awk -v n=3 -v i="$SHARD" 'NR % n == i % n') + echo "Shard $SHARD/3: ${#files[@]} files" # shellcheck disable=SC2086 # COVERAGE is one flag or nothing. - flutter test --exclude-tags arduino $COVERAGE \ - --total-shards 3 --shard-index $(( ${{ matrix.shard }} - 1 )) + flutter test --exclude-tags arduino $COVERAGE "${files[@]}" - name: Upload coverage if: github.event_name == 'push' From a3917c986c87dddc16c30e1615a7ba8f2d0ffba6 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Sun, 4 Oct 2026 20:20:55 +0200 Subject: [PATCH 2/3] Cache the analyzer's work, and skip the web build when it can't change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Analyze keeps `dart analyze`'s own cache — summaries of the SDK, the dependencies and every library it has seen — between runs. Locally that took analysis from 48 s with an empty cache to 27 s warm, for 76 MB. - A pull request that touches only what the web build does not compile (tests, the native runners, the git hooks) skips it. Checked inside the job, so the web build — now CI's longest — does not wait for `detect` first. Workflow changes still build: they can break the job. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29d840d..a9cd6e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,10 +70,22 @@ jobs: # own, where `flutter analyze` exited 0 with "No issues found". Verified # by pinning riverpod_lint to a nonexistent version — the message it # greps for is still the one printed. + # The analyzer's own cache — summaries of the SDK, every dependency and + # each library it has seen — kept between runs. Measured locally: 48 s + # with it empty, 27 s warm, for 76 MB. Keyed on the sources so an exact + # hit is this tree; a partial one still carries everything unchanged, + # since entries are addressed by content. + - name: Restore the analysis cache + uses: actions/cache@v6 + with: + path: .dart_tool/analysis-cache + key: analysis-${{ runner.os }}-${{ hashFiles('pubspec.lock', 'analysis_options.yaml', 'lib/**/*.dart', 'test/**/*.dart') }} + restore-keys: analysis-${{ runner.os }}- + - name: Analyze run: | set -o pipefail - dart analyze --fatal-infos 2>&1 | tee analyze.log + dart analyze --fatal-infos --cache=.dart_tool/analysis-cache 2>&1 | tee analyze.log if grep -q "error occurred while executing an analyzer plugin" analyze.log; then echo "::error::An analyzer plugin failed to load — see the log above." exit 1 @@ -188,11 +200,37 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v7 + + # A pull request that touches only what the web build does not compile — + # tests, the native runners, the git hooks — skips it. Checked here + # rather than by `detect`, which would make every web build wait for that + # job first, and the web build is CI's longest job. Anything else builds, + # workflow changes included, since they can break this job itself; so do + # pushes to main, which a web preview follows. + - name: Does this change reach the web build? + id: web + env: + EVENT: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + set -euo pipefail + needed=true + if [ "$EVENT" = pull_request ] && git fetch -q --no-tags --depth=1 origin "$BASE_SHA"; then + if ! git diff --name-only "$BASE_SHA" HEAD \ + | grep -qvE '^(test/|\.githooks/|setup_hooks\.sh$|(macos|windows|linux|ios|android)/)'; then + needed=false + echo "::notice::Nothing this pull request changes is compiled by the web build — skipped." + fi + fi + echo "needed=$needed" >> "$GITHUB_OUTPUT" + - uses: ./.github/actions/setup-app + if: steps.web.outputs.needed == 'true' # --wasm because that is the configuration that ships; a dart2js-only # build would miss a WASM-specific compile failure. - name: Build (WASM) + if: steps.web.outputs.needed == 'true' run: flutter build web --wasm --pwa-strategy=none # What the rest of the run needs to know about this push or pull request: From 9d1a0c40e369a13b8b6be31fc19691995b4c2b2d Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Sun, 4 Oct 2026 20:35:54 +0200 Subject: [PATCH 3/3] Give the analyzer an absolute cache path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A relative --cache is resolved against the analyzer's own install for the plugin builds it keeps there, so riverpod_lint failed to load and Analyze exited 4 — caught by the plugin-load guard. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9cd6e6..8e826b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,7 +85,10 @@ jobs: - name: Analyze run: | set -o pipefail - dart analyze --fatal-infos --cache=.dart_tool/analysis-cache 2>&1 | tee analyze.log + # Absolute: the analyzer resolves a relative --cache against its own + # install for the plugin builds it keeps there, and riverpod_lint + # then fails to load ("does not contain a valid AOT snapshot"). + dart analyze --fatal-infos --cache="$PWD/.dart_tool/analysis-cache" 2>&1 | tee analyze.log if grep -q "error occurred while executing an analyzer plugin" analyze.log; then echo "::error::An analyzer plugin failed to load — see the log above." exit 1