Skip to content

Commit 316b915

Browse files
xsahil03xclaude
andcommitted
refactor(ci): scope PR jobs to packages changed by the PR
Adds `analyze:changes`, `format:changes`, `test:changes`, and `lint:pub:changes` melos scripts that run only on packages a PR touches (plus their transitive dependents). The diff range is `HEAD~1...HEAD`, which works for both PR events (HEAD~1 is the base-branch tip on GitHub's merge ref, regardless of which branch the PR targets) and push events (HEAD~1 is the previous tip). `test:changes` delegates to `.github/workflows/scripts/test-package.sh` because melos applies packageFilters BEFORE `--include-dependents`, so examples (no test/ directory, but path-depend on every changed package) get pulled back into the set via transitive deps and can't be filtered out by `dirExists`. The wrapper skips them per-package after the affected set is computed, with explicit exit codes so real test failures propagate. Coverage checks gain `hashFiles()` guards so they skip cleanly when their package wasn't touched on this PR. Codecov upload moves to last so we never push coverage that failed local thresholds. The legacy `ACTIONS_ALLOW_UNSECURE_COMMANDS` env var is removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4103da3 commit 316b915

3 files changed

Lines changed: 96 additions & 14 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
3+
# Per-package test runner invoked from `melos run test:changes`.
4+
#
5+
# Why this exists: in `melos exec`, `--include-dependents` runs AFTER all
6+
# package filters (see `applyFilters` in melos's package.dart). When a change
7+
# affects e.g. `stream_chat`, melos pulls in every transitive dependent from
8+
# the unfiltered workspace — including `*_example` packages that have no
9+
# `test/` directory and `sample_app/` which we don't test in this matrix.
10+
# Filtering via `dirExists: test` or `--ignore="*example*"` doesn't help
11+
# because those filters are bypassed for dependents.
12+
#
13+
# So the skip decision happens here, after melos has computed the affected
14+
# set. `set -e` plus an explicit `exec` ensures real test failures propagate
15+
# (the original inline `[ -d test ] && X || Y` workaround silently swallowed
16+
# failures when `flutter test` exited non-zero).
17+
18+
set -e
19+
20+
if [[ "$MELOS_PACKAGE_NAME" == *_example ]]; then
21+
echo "→ Skipping example: $MELOS_PACKAGE_NAME"
22+
exit 0
23+
fi
24+
25+
if [ ! -d test ]; then
26+
echo "→ No test/ in $MELOS_PACKAGE_NAME, skipping"
27+
exit 0
28+
fi
29+
30+
exec flutter test --no-pub --coverage

.github/workflows/stream_flutter_workflow.yml

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
name: stream_flutter_workflow
22

33
env:
4-
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
54
flutter_version: "3.x"
65

76
on:
@@ -27,8 +26,10 @@ jobs:
2726
# filtering, which hangs forever) — see
2827
# https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks
2928
#
30-
# `push` events to master always run the full suite, matching the
31-
# previous `on.push.branches: [master]` behavior.
29+
# `push` events always run (no draft/path gate), but downstream jobs still
30+
# use the diff-aware `:changes` scripts. On a master push, `HEAD~1` is the
31+
# previous tip, so the scope is exactly the merged commit's changes —
32+
# matching what the PR ran.
3233
gate:
3334
runs-on: ubuntu-latest
3435
outputs:
@@ -69,12 +70,13 @@ jobs:
6970
- name: "Bootstrap Workspace"
7071
run: melos bootstrap --verbose
7172
- name: "Dart Analyze"
72-
run: |
73-
melos run analyze
73+
run: melos run analyze:changes
74+
# Only on PRs targeting master — feature branches (v10, design-refresh,
75+
# etc.) often carry unreleased version numbers that aren't ready for a
76+
# publish dry-run yet.
7477
- name: "Pub Check"
7578
if: github.base_ref == 'master'
76-
run: |
77-
melos run lint:pub
79+
run: melos run lint:pub:changes
7880

7981
format:
8082
needs: gate
@@ -98,7 +100,7 @@ jobs:
98100
- name: "Bootstrap Workspace"
99101
run: melos bootstrap
100102
- name: "Melos Format"
101-
run: melos run format
103+
run: melos run format:changes
102104
- name: "Validate Formatting"
103105
run: |
104106
./.github/workflows/scripts/validate-formatting.sh
@@ -129,39 +131,48 @@ jobs:
129131
- name: "Bootstrap Workspace"
130132
run: melos bootstrap
131133
- name: "Flutter Test"
132-
run: melos run test:all
134+
run: melos run test:changes
133135
- name: "Collect Coverage"
134136
run: melos run coverage:ignore-file --no-select
135-
- name: "Upload Coverage"
136-
uses: codecov/codecov-action@v5
137-
with:
138-
token: ${{secrets.CODECOV_TOKEN}}
139-
files: packages/*/coverage/lcov.info
137+
# Coverage thresholds skip packages whose tests didn't run on this PR —
138+
# `hashFiles` returns an empty string when the path is missing. Without
139+
# this guard, the action errors on missing files for unaffected packages.
140140
- name: "Stream Chat Coverage Check"
141+
if: hashFiles('packages/stream_chat/coverage/lcov.info') != ''
141142
uses: VeryGoodOpenSource/very_good_coverage@v3.0.0
142143
with:
143144
path: packages/stream_chat/coverage/lcov.info
144145
min_coverage: 70
145146
- name: "Stream Chat Localizations Coverage Check"
147+
if: hashFiles('packages/stream_chat_localizations/coverage/lcov.info') != ''
146148
uses: VeryGoodOpenSource/very_good_coverage@v3.0.0
147149
with:
148150
path: packages/stream_chat_localizations/coverage/lcov.info
149151
min_coverage: 100
150152
- name: "Stream Chat Persistence Coverage Check"
153+
if: hashFiles('packages/stream_chat_persistence/coverage/lcov.info') != ''
151154
uses: VeryGoodOpenSource/very_good_coverage@v3.0.0
152155
with:
153156
path: packages/stream_chat_persistence/coverage/lcov.info
154157
min_coverage: 95
155158
- name: "Stream Chat Flutter Core Coverage Check"
159+
if: hashFiles('packages/stream_chat_flutter_core/coverage/lcov.info') != ''
156160
uses: VeryGoodOpenSource/very_good_coverage@v3.0.0
157161
with:
158162
path: packages/stream_chat_flutter_core/coverage/lcov.info
159163
min_coverage: 30
160164
- name: "Stream Chat Flutter Coverage Check"
165+
if: hashFiles('packages/stream_chat_flutter/coverage/lcov.info') != ''
161166
uses: VeryGoodOpenSource/very_good_coverage@v3.0.0
162167
with:
163168
path: packages/stream_chat_flutter/coverage/lcov.info
164169
min_coverage: 44
170+
# Upload last so we never push coverage that failed local thresholds.
171+
- name: "Upload Coverage"
172+
uses: codecov/codecov-action@v5
173+
with:
174+
token: ${{secrets.CODECOV_TOKEN}}
175+
files: packages/*/coverage/lcov.info
165176

166177
build:
167178
name: build (${{ matrix.platform }})

melos.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,32 @@ scripts:
135135
Run `dart analyze` in all packages.
136136
- Note: you can also rely on your IDEs Dart Analysis / Issues window.
137137
138+
# Scoped to packages touched by the current branch (plus their dependents),
139+
# so PR CI only analyses what the PR could have broken. Examples are
140+
# intentionally included — analysing them catches API drift in the sample
141+
# code we ship.
142+
#
143+
# Why `HEAD~1...HEAD`: GitHub's `actions/checkout` checks out the PR's
144+
# auto-generated merge commit (`refs/pull/N/merge`) by default. On that
145+
# commit, `HEAD~1` is the first parent — i.e. the PR's BASE branch tip
146+
# (master, v10, design-refresh, whatever). So `HEAD~1...HEAD` gives the
147+
# PR's own changes regardless of which branch it targets. On `push` events
148+
# `HEAD~1` is just the previous tip, so the same range gives the pushed
149+
# commit's changes. No workflow env plumbing needed.
150+
analyze:changes:
151+
run: melos exec -c 5 --diff="HEAD~1...HEAD" --include-dependents -- "dart analyze --fatal-infos ."
152+
description: Run `dart analyze` in packages changed since HEAD~1.
153+
138154
format:
139155
run: dart format --set-exit-if-changed .
140156
description: |
141157
Run `dart format --set-exit-if-changed .` in all packages.
142158
159+
# Same scoping as `analyze:changes`, same reasoning for including examples.
160+
format:changes:
161+
run: melos exec -c 5 --diff="HEAD~1...HEAD" --include-dependents -- "dart format --set-exit-if-changed ."
162+
description: Run `dart format` in packages changed since HEAD~1.
163+
143164
metrics:
144165
run: |
145166
melos exec -c 1 --ignore="*example*" -- \
@@ -152,6 +173,16 @@ scripts:
152173
run: melos exec -c 1 --no-published --no-private --order-dependents -- "flutter pub publish -n"
153174
description: Dry run `pub publish` in all packages.
154175

176+
# Mirrors the filter shape of `lint:pub` (no `--include-dependents`); just
177+
# adds diff-scoping. `pub publish --dry-run` validates this package's own
178+
# pubspec/files/metadata and never reads the parent's code, so dependent
179+
# expansion would add CI time without catching anything new. `--no-private`
180+
# filters examples cleanly here because we're not using --include-dependents
181+
# (the filter-bypass quirk only triggers during dependent expansion).
182+
lint:pub:changes:
183+
run: melos exec -c 1 --diff="HEAD~1...HEAD" --no-published --no-private --order-dependents -- "flutter pub publish -n"
184+
description: Dry run `pub publish` in publishable packages changed since HEAD~1.
185+
155186
generate:all:
156187
run: melos run generate:dart && melos run generate:flutter
157188
description: Build all generated files for Dart & Flutter packages in this project.
@@ -186,6 +217,16 @@ scripts:
186217
flutter: true
187218
dirExists: test
188219

220+
# Examples are in the workspace (needed for analyze/format coverage) but
221+
# have no tests. `packageFilters` is applied before `--include-dependents`
222+
# in melos, so we can't filter examples out via a filter — they re-enter
223+
# the set through transitive-dependent expansion. The wrapper script does
224+
# the skip per-package after melos has done its math. `--no-pub` is safe
225+
# because the workflow runs `melos bootstrap` first.
226+
test:changes:
227+
run: melos exec -c 4 --fail-fast --diff="HEAD~1...HEAD" --include-dependents -- "bash \$MELOS_ROOT_PATH/.github/workflows/scripts/test-package.sh"
228+
description: Run `flutter test` in packages changed since HEAD~1.
229+
189230
update:goldens:
190231
run: melos exec -c 1 --depends-on="alchemist" -- "flutter test --tags golden --update-goldens"
191232
description: Update golden files for all packages in this project.

0 commit comments

Comments
 (0)