From 316b9155cc01fa1e1ff34972e3738da1e13eb4d6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 21 May 2026 16:10:11 +0200 Subject: [PATCH 1/2] 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) --- .github/workflows/scripts/test-package.sh | 30 ++++++++++++++ .github/workflows/stream_flutter_workflow.yml | 39 +++++++++++------- melos.yaml | 41 +++++++++++++++++++ 3 files changed, 96 insertions(+), 14 deletions(-) create mode 100755 .github/workflows/scripts/test-package.sh diff --git a/.github/workflows/scripts/test-package.sh b/.github/workflows/scripts/test-package.sh new file mode 100755 index 0000000000..fb53a88fc1 --- /dev/null +++ b/.github/workflows/scripts/test-package.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Per-package test runner invoked from `melos run test:changes`. +# +# Why this exists: in `melos exec`, `--include-dependents` runs AFTER all +# package filters (see `applyFilters` in melos's package.dart). When a change +# affects e.g. `stream_chat`, melos pulls in every transitive dependent from +# the unfiltered workspace — including `*_example` packages that have no +# `test/` directory and `sample_app/` which we don't test in this matrix. +# Filtering via `dirExists: test` or `--ignore="*example*"` doesn't help +# because those filters are bypassed for dependents. +# +# So the skip decision happens here, after melos has computed the affected +# set. `set -e` plus an explicit `exec` ensures real test failures propagate +# (the original inline `[ -d test ] && X || Y` workaround silently swallowed +# failures when `flutter test` exited non-zero). + +set -e + +if [[ "$MELOS_PACKAGE_NAME" == *_example ]]; then + echo "→ Skipping example: $MELOS_PACKAGE_NAME" + exit 0 +fi + +if [ ! -d test ]; then + echo "→ No test/ in $MELOS_PACKAGE_NAME, skipping" + exit 0 +fi + +exec flutter test --no-pub --coverage diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index c9c1b19eff..54d6b4e885 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -1,7 +1,6 @@ name: stream_flutter_workflow env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' flutter_version: "3.x" on: @@ -27,8 +26,10 @@ jobs: # filtering, which hangs forever) — see # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks # - # `push` events to master always run the full suite, matching the - # previous `on.push.branches: [master]` behavior. + # `push` events always run (no draft/path gate), but downstream jobs still + # use the diff-aware `:changes` scripts. On a master push, `HEAD~1` is the + # previous tip, so the scope is exactly the merged commit's changes — + # matching what the PR ran. gate: runs-on: ubuntu-latest outputs: @@ -69,12 +70,13 @@ jobs: - name: "Bootstrap Workspace" run: melos bootstrap --verbose - name: "Dart Analyze" - run: | - melos run analyze + run: melos run analyze:changes + # Only on PRs targeting master — feature branches (v10, design-refresh, + # etc.) often carry unreleased version numbers that aren't ready for a + # publish dry-run yet. - name: "Pub Check" if: github.base_ref == 'master' - run: | - melos run lint:pub + run: melos run lint:pub:changes format: needs: gate @@ -98,7 +100,7 @@ jobs: - name: "Bootstrap Workspace" run: melos bootstrap - name: "Melos Format" - run: melos run format + run: melos run format:changes - name: "Validate Formatting" run: | ./.github/workflows/scripts/validate-formatting.sh @@ -129,39 +131,48 @@ jobs: - name: "Bootstrap Workspace" run: melos bootstrap - name: "Flutter Test" - run: melos run test:all + run: melos run test:changes - name: "Collect Coverage" run: melos run coverage:ignore-file --no-select - - name: "Upload Coverage" - uses: codecov/codecov-action@v5 - with: - token: ${{secrets.CODECOV_TOKEN}} - files: packages/*/coverage/lcov.info + # Coverage thresholds skip packages whose tests didn't run on this PR — + # `hashFiles` returns an empty string when the path is missing. Without + # this guard, the action errors on missing files for unaffected packages. - name: "Stream Chat Coverage Check" + if: hashFiles('packages/stream_chat/coverage/lcov.info') != '' uses: VeryGoodOpenSource/very_good_coverage@v3.0.0 with: path: packages/stream_chat/coverage/lcov.info min_coverage: 70 - name: "Stream Chat Localizations Coverage Check" + if: hashFiles('packages/stream_chat_localizations/coverage/lcov.info') != '' uses: VeryGoodOpenSource/very_good_coverage@v3.0.0 with: path: packages/stream_chat_localizations/coverage/lcov.info min_coverage: 100 - name: "Stream Chat Persistence Coverage Check" + if: hashFiles('packages/stream_chat_persistence/coverage/lcov.info') != '' uses: VeryGoodOpenSource/very_good_coverage@v3.0.0 with: path: packages/stream_chat_persistence/coverage/lcov.info min_coverage: 95 - name: "Stream Chat Flutter Core Coverage Check" + if: hashFiles('packages/stream_chat_flutter_core/coverage/lcov.info') != '' uses: VeryGoodOpenSource/very_good_coverage@v3.0.0 with: path: packages/stream_chat_flutter_core/coverage/lcov.info min_coverage: 30 - name: "Stream Chat Flutter Coverage Check" + if: hashFiles('packages/stream_chat_flutter/coverage/lcov.info') != '' uses: VeryGoodOpenSource/very_good_coverage@v3.0.0 with: path: packages/stream_chat_flutter/coverage/lcov.info min_coverage: 44 + # Upload last so we never push coverage that failed local thresholds. + - name: "Upload Coverage" + uses: codecov/codecov-action@v5 + with: + token: ${{secrets.CODECOV_TOKEN}} + files: packages/*/coverage/lcov.info build: name: build (${{ matrix.platform }}) diff --git a/melos.yaml b/melos.yaml index 5b7c7e2e07..2a2ebf96d5 100644 --- a/melos.yaml +++ b/melos.yaml @@ -135,11 +135,32 @@ scripts: Run `dart analyze` in all packages. - Note: you can also rely on your IDEs Dart Analysis / Issues window. + # Scoped to packages touched by the current branch (plus their dependents), + # so PR CI only analyses what the PR could have broken. Examples are + # intentionally included — analysing them catches API drift in the sample + # code we ship. + # + # Why `HEAD~1...HEAD`: GitHub's `actions/checkout` checks out the PR's + # auto-generated merge commit (`refs/pull/N/merge`) by default. On that + # commit, `HEAD~1` is the first parent — i.e. the PR's BASE branch tip + # (master, v10, design-refresh, whatever). So `HEAD~1...HEAD` gives the + # PR's own changes regardless of which branch it targets. On `push` events + # `HEAD~1` is just the previous tip, so the same range gives the pushed + # commit's changes. No workflow env plumbing needed. + analyze:changes: + run: melos exec -c 5 --diff="HEAD~1...HEAD" --include-dependents -- "dart analyze --fatal-infos ." + description: Run `dart analyze` in packages changed since HEAD~1. + format: run: dart format --set-exit-if-changed . description: | Run `dart format --set-exit-if-changed .` in all packages. + # Same scoping as `analyze:changes`, same reasoning for including examples. + format:changes: + run: melos exec -c 5 --diff="HEAD~1...HEAD" --include-dependents -- "dart format --set-exit-if-changed ." + description: Run `dart format` in packages changed since HEAD~1. + metrics: run: | melos exec -c 1 --ignore="*example*" -- \ @@ -152,6 +173,16 @@ scripts: run: melos exec -c 1 --no-published --no-private --order-dependents -- "flutter pub publish -n" description: Dry run `pub publish` in all packages. + # Mirrors the filter shape of `lint:pub` (no `--include-dependents`); just + # adds diff-scoping. `pub publish --dry-run` validates this package's own + # pubspec/files/metadata and never reads the parent's code, so dependent + # expansion would add CI time without catching anything new. `--no-private` + # filters examples cleanly here because we're not using --include-dependents + # (the filter-bypass quirk only triggers during dependent expansion). + lint:pub:changes: + run: melos exec -c 1 --diff="HEAD~1...HEAD" --no-published --no-private --order-dependents -- "flutter pub publish -n" + description: Dry run `pub publish` in publishable packages changed since HEAD~1. + generate:all: run: melos run generate:dart && melos run generate:flutter description: Build all generated files for Dart & Flutter packages in this project. @@ -186,6 +217,16 @@ scripts: flutter: true dirExists: test + # Examples are in the workspace (needed for analyze/format coverage) but + # have no tests. `packageFilters` is applied before `--include-dependents` + # in melos, so we can't filter examples out via a filter — they re-enter + # the set through transitive-dependent expansion. The wrapper script does + # the skip per-package after melos has done its math. `--no-pub` is safe + # because the workflow runs `melos bootstrap` first. + test:changes: + run: melos exec -c 4 --fail-fast --diff="HEAD~1...HEAD" --include-dependents -- "bash \$MELOS_ROOT_PATH/.github/workflows/scripts/test-package.sh" + description: Run `flutter test` in packages changed since HEAD~1. + update:goldens: run: melos exec -c 1 --depends-on="alchemist" -- "flutter test --tags golden --update-goldens" description: Update golden files for all packages in this project. From 082bafcbd1727fdb23020b00da9396f1d0e1ff65 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 21 May 2026 17:52:35 +0200 Subject: [PATCH 2/2] chore(repo): drop dart_code_metrics, modernize build_runner, guard cycles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove `metrics` + `analyze:all` scripts. dart_code_metrics's OSS version was discontinued in 2024 (the maintainer pivoted to commercial DCM); the script wasn't actually installed anywhere, so the command would have failed if anyone ran it. - Switch `generate:dart` and `generate:flutter` to `dart run build_runner`. `flutter pub run` has been deprecated since Flutter 3.7 (Feb 2023); `dart run` works for both Dart and Flutter packages and skips the Flutter binary warmup. Also moved both scripts to melos's `exec:` shorthand — `packageFilters` is now a first-class sibling key, no more `--depends-on=... --no-flutter` flag parsing in a quoted string. - Add `lint:cycles` script (wraps `melos list --cycles`) and run it before bootstrap in the analyze job. Fails fast if anyone introduces a circular dep — ~1s to run, saves the full bootstrap cost on a doomed PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/stream_flutter_workflow.yml | 5 ++++ melos.yaml | 30 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 54d6b4e885..a6ca0db8a7 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -67,6 +67,11 @@ jobs: - name: "Install Tools" run: | flutter pub global activate melos + # Fail fast on circular deps — runs in ~1s and saves the bootstrap cost + # on a doomed run. Only added in this job (one failure fails the + # workflow via downstream `needs:`). + - name: "Lint Cycles" + run: melos run lint:cycles - name: "Bootstrap Workspace" run: melos bootstrap --verbose - name: "Dart Analyze" diff --git a/melos.yaml b/melos.yaml index 2a2ebf96d5..3fab0b2acb 100644 --- a/melos.yaml +++ b/melos.yaml @@ -123,10 +123,6 @@ scripts: run: melos run analyze && melos run format description: Run all static analysis checks - analyze:all: - run: melos run analyze && melos run metrics - description: Run all - analyze: run: | melos exec -c 5 --ignore="*example*" -- \ @@ -161,13 +157,11 @@ scripts: run: melos exec -c 5 --diff="HEAD~1...HEAD" --include-dependents -- "dart format --set-exit-if-changed ." description: Run `dart format` in packages changed since HEAD~1. - metrics: - run: | - melos exec -c 1 --ignore="*example*" -- \ - flutter pub run dart_code_metrics:metrics analyze lib - description: | - Run `dart_code_metrics` in all packages. - - Note: you can also rely on your IDEs Dart Analysis / Issues window. + # Fails on circular package dependencies — cheap defensive check to run + # in CI before bootstrap, since a cycle here would silently break everyone. + lint:cycles: + run: melos list --cycles + description: Fail if any cycle exists in the workspace dependency graph. lint:pub: run: melos exec -c 1 --no-published --no-private --order-dependents -- "flutter pub publish -n" @@ -187,12 +181,22 @@ scripts: run: melos run generate:dart && melos run generate:flutter description: Build all generated files for Dart & Flutter packages in this project. + # `dart run build_runner` works for both Dart and Flutter packages; the + # `flutter pub run` form is deprecated since Flutter 3.7 and slower (it + # warms up the Flutter binary unnecessarily). One script body covers both + # packageFilters splits via the `flutter:` filter. generate:dart: - run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs" + exec: dart run build_runner build --delete-conflicting-outputs + packageFilters: + flutter: false + dependsOn: build_runner description: Build all generated files for Dart packages in this project. generate:flutter: - run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs" + exec: dart run build_runner build --delete-conflicting-outputs + packageFilters: + flutter: true + dependsOn: build_runner description: Build all generated files for Flutter packages in this project. version:update: