Condense firehose package publishing table in PR validation (#445)

Condense the package publishing table posted by firehose during PR validation to reduce noise in monorepos.

* Only include packages affected by the PR, packages ready to publish, and packages with validation errors in the markdown table.
* Summarize unaffected WIP and already published packages below the table.
* Add unit and integration tests covering table filtering and summary generation.
diff --git a/pkgs/firehose/CHANGELOG.md b/pkgs/firehose/CHANGELOG.md
index 7faab47..3b0e0f9 100644
--- a/pkgs/firehose/CHANGELOG.md
+++ b/pkgs/firehose/CHANGELOG.md
@@ -1,5 +1,8 @@
 ## 0.13.2-wip
 
+- Condense PR package publishing table to show only packages affected by the PR,
+  packages ready to publish, and errors, summarizing unaffected WIP and published
+  packages.
 - Remove 3rd-party Coveralls action and `upload_coverage` input from health workflows.
 - Add `--tag-prefix` option to `firehose` and `tag-prefix` input to `publish.yaml` to allow configuring the release tag prefix (defaults to `'v'`).
 - Run `pub get` before `dart format` in `groundskeeper`.
diff --git a/pkgs/firehose/lib/firehose.dart b/pkgs/firehose/lib/firehose.dart
index cd74f6f..965f8e3 100644
--- a/pkgs/firehose/lib/firehose.dart
+++ b/pkgs/firehose/lib/firehose.dart
@@ -110,11 +110,20 @@
     final repo = Repository(directory);
     final packages = repo.locatePackages(ignore: ignoredPackages);
 
+    final filesInPR = await allowFailure(
+      github.listFilesForPR(directory, ignoredPackages),
+      logError: print,
+    );
+    final relevantFiles = filesInPR?.where((f) => f.status.isRelevant).toList();
+
     final pub = Pub();
 
     final results = VerificationResults();
 
     for (final package in packages) {
+      final isAffected = relevantFiles == null ||
+          relevantFiles.any((f) => f.isInPackage(package));
+
       final repoTag = repo.calculateRepoTag(package, tagPrefix: tagPrefix);
 
       print('');
@@ -126,6 +135,7 @@
         final result = Result.fail(
           package,
           "no version specified (perhaps you need a' publish_to: none' entry?)",
+          isAffected: isAffected,
         );
         print(result);
         results.addResult(result);
@@ -142,6 +152,7 @@
           package,
           'pubspec version ($pubspecVersion) and changelog ($changelogVersion) '
           "don't agree",
+          isAffected: isAffected,
         );
         print(result);
         results.addResult(result);
@@ -149,11 +160,19 @@
       }
 
       if (await pub.hasPublishedVersion(package.name, pubspecVersion)) {
-        final result = Result.info(package, 'already published at pub.dev');
+        final result = Result.info(
+          package,
+          Result.alreadyPublishedMessage,
+          isAffected: isAffected,
+        );
         print(result);
         results.addResult(result);
       } else if (package.pubspec.version!.wip) {
-        final result = Result.info(package, 'WIP (no publish necessary)');
+        final result = Result.info(
+          package,
+          Result.wipMessage,
+          isAffected: isAffected,
+        );
         print(result);
         results.addResult(result);
       } else {
@@ -172,17 +191,20 @@
               'pub publish dry-run failed; add the `$_ignoreWarningsLabel` '
               'label to ignore';
           github.notice(message: message);
-          results.addResult(Result.fail(package, message));
+          results.addResult(
+            Result.fail(package, message, isAffected: isAffected),
+          );
         } else {
           final result = Result.success(
             package,
             '**ready to publish**',
-            repoTag,
-            repo.calculateReleaseUri(
+            gitTag: repoTag,
+            publishReleaseUri: repo.calculateReleaseUri(
               package,
               github,
               tagPrefix: tagPrefix,
             ),
+            isAffected: isAffected,
           );
           print(result);
           results.addResult(result);
@@ -323,29 +345,69 @@
 
   bool get hasError => results.any((r) => r.severity == Severity.error);
 
-  String describeAsMarkdown({bool withTag = true}) => results.map((r) {
-        final sev = r.severity == Severity.error ? '(error) ' : '';
-        var tagColumn = '';
-        if (withTag) {
-          var tag = r.gitTag == null ? '' : '`${r.gitTag}`';
-          final publishReleaseUri = r.publishReleaseUri;
-          if (publishReleaseUri != null) {
-            tag = '[$tag]($publishReleaseUri)';
-          }
+  Iterable<Result> get visibleResults =>
+      results.where((r) => r.isVisibleInTable);
 
-          tagColumn = ' | $tag';
+  Iterable<Result> get hiddenResults =>
+      results.where((r) => !r.isVisibleInTable);
+
+  String describeAsMarkdown({bool withTag = true}) {
+    final buffer = StringBuffer();
+    for (final r in visibleResults) {
+      final sev = r.severity == Severity.error ? '(error) ' : '';
+      var tagColumn = '';
+      if (withTag) {
+        var tag = r.gitTag == null ? '' : '`${r.gitTag}`';
+        final publishReleaseUri = r.publishReleaseUri;
+        if (publishReleaseUri != null) {
+          tag = '[$tag]($publishReleaseUri)';
         }
-        return '| package:${r.package.name} | ${r.package.version} | '
-            '$sev${r.message}$tagColumn |';
-      }).join('\n');
+
+        tagColumn = ' | $tag';
+      }
+      final pkgName = r.isAffected
+          ? '**package:${r.package.name}** ⭐'
+          : 'package:${r.package.name}';
+      buffer.writeln(
+        '| $pkgName | ${r.package.version} | '
+        '$sev${r.message}$tagColumn |',
+      );
+    }
+
+    final hidden = hiddenResults.toList();
+    final alreadyPublishedCount =
+        hidden.where((r) => r.message == Result.alreadyPublishedMessage).length;
+    final wipCount = hidden.where((r) => r.message == Result.wipMessage).length;
+
+    final summaryLines = <String>[];
+    if (alreadyPublishedCount > 0) {
+      summaryLines.add('* $alreadyPublishedCount already published.');
+    }
+    if (wipCount > 0) {
+      summaryLines.add('* $wipCount WIP (no publish necessary).');
+    }
+
+    if (summaryLines.isNotEmpty) {
+      if (visibleResults.isNotEmpty) {
+        buffer.writeln();
+      }
+      buffer.write(summaryLines.join('\n'));
+    }
+
+    return buffer.toString().trimRight();
+  }
 }
 
 class Result {
+  static const String alreadyPublishedMessage = 'already published at pub.dev';
+  static const String wipMessage = 'WIP (no publish necessary)';
+
   final Severity severity;
   final Package package;
   final String message;
   final String? gitTag;
   final Uri? publishReleaseUri;
+  final bool isAffected;
 
   Result(
     this.severity,
@@ -353,21 +415,35 @@
     this.message, [
     this.gitTag,
     this.publishReleaseUri,
+    this.isAffected = false,
   ]);
 
-  factory Result.fail(Package package, String message) =>
-      Result(Severity.error, package, message);
+  factory Result.fail(
+    Package package,
+    String message, {
+    bool isAffected = false,
+  }) =>
+      Result(Severity.error, package, message, null, null, isAffected);
 
-  factory Result.info(Package package, String message) =>
-      Result(Severity.info, package, message);
+  factory Result.info(
+    Package package,
+    String message, {
+    bool isAffected = false,
+  }) =>
+      Result(Severity.info, package, message, null, null, isAffected);
 
   factory Result.success(
     Package package,
-    String message, [
+    String message, {
     String? gitTag,
     Uri? publishReleaseUri,
-  ]) =>
-      Result(Severity.success, package, message, gitTag, publishReleaseUri);
+    bool isAffected = false,
+  }) =>
+      Result(Severity.success, package, message, gitTag, publishReleaseUri,
+          isAffected);
+
+  bool get isVisibleInTable =>
+      isAffected || severity == Severity.success || severity == Severity.error;
 
   @override
   String toString() {
diff --git a/pkgs/firehose/test/firehose_test.dart b/pkgs/firehose/test/firehose_test.dart
new file mode 100644
index 0000000..0f553c6
--- /dev/null
+++ b/pkgs/firehose/test/firehose_test.dart
@@ -0,0 +1,143 @@
+// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+
+import 'package:firehose/firehose.dart';
+import 'package:firehose/src/local_github_api.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('VerificationResults', () {
+    late Repository repo;
+    late Package pkgA;
+    late Package pkgB;
+    late Package pkgC;
+    late Package pkgD;
+    late Package pkgE;
+
+    setUp(() {
+      repo = Repository();
+      pkgA = Package(Directory('test_data/workspace_repo/pkg_1'), repo);
+      pkgB = Package(Directory('test_data/workspace_repo/pkg_2'), repo);
+      pkgC = Package(Directory('test_data/test_repo/pkgs/package1'), repo);
+      pkgD = Package(Directory('test_data/test_repo/pkgs/package2'), repo);
+      pkgE = Package(Directory('test_data/test_repo/pkgs/package3'), repo);
+    });
+
+    test('all packages affected are visible in table and highlighted', () {
+      final results = VerificationResults()
+        ..addResult(Result.info(pkgA, Result.wipMessage, isAffected: true))
+        ..addResult(Result.info(pkgB, Result.alreadyPublishedMessage,
+            isAffected: true));
+
+      expect(results.visibleResults.length, 2);
+      expect(results.hiddenResults, isEmpty);
+
+      final markdown = results.describeAsMarkdown(withTag: false);
+      expect(markdown, contains('| **package:pkg_1** ⭐ |'));
+      expect(markdown, contains('| **package:pkg_2** ⭐ |'));
+      expect(markdown, isNot(contains('already published.')));
+      expect(markdown, isNot(contains('WIP (no publish necessary).')));
+    });
+
+    test('unaffected ready-to-publish and error packages are not starred', () {
+      final results = VerificationResults()
+        ..addResult(
+            Result.success(pkgA, '**ready to publish**', isAffected: false))
+        ..addResult(Result.fail(pkgB, 'version mismatch', isAffected: false));
+
+      expect(results.visibleResults.length, 2);
+      expect(results.hiddenResults, isEmpty);
+
+      final markdown = results.describeAsMarkdown(withTag: false);
+      expect(markdown, contains('| package:pkg_1 |'));
+      expect(markdown, contains('| package:pkg_2 |'));
+      expect(markdown, isNot(contains('⭐')));
+    });
+
+    test('unaffected WIP and already-published packages are summarized', () {
+      final results = VerificationResults()
+        ..addResult(
+            Result.success(pkgA, '**ready to publish**', isAffected: false))
+        ..addResult(Result.info(pkgB, Result.wipMessage, isAffected: true))
+        ..addResult(Result.info(pkgC, Result.wipMessage, isAffected: false))
+        ..addResult(Result.info(pkgD, Result.wipMessage, isAffected: false))
+        ..addResult(Result.info(pkgE, Result.alreadyPublishedMessage,
+            isAffected: false));
+
+      expect(results.visibleResults.length, 2);
+      expect(results.hiddenResults.length, 3);
+
+      final markdown = results.describeAsMarkdown(withTag: false);
+      expect(markdown, contains('| package:pkg_1 |'));
+      expect(markdown, contains('| **package:pkg_2** ⭐ |'));
+      expect(markdown, isNot(contains('| package:package1 |')));
+      expect(markdown, isNot(contains('| package:package2 |')));
+      expect(markdown, isNot(contains('| package:package3 |')));
+
+      expect(markdown, contains('* 1 already published.'));
+      expect(markdown, contains('* 2 WIP (no publish necessary).'));
+    });
+
+    test('summary omitted when counts are zero', () {
+      final results = VerificationResults()
+        ..addResult(
+            Result.success(pkgA, '**ready to publish**', isAffected: true));
+
+      final markdown = results.describeAsMarkdown(withTag: false);
+      expect(markdown, contains('| **package:pkg_1** ⭐ |'));
+      expect(markdown, isNot(contains('already published.')));
+      expect(markdown, isNot(contains('WIP (no publish necessary).')));
+    });
+
+    test('only summary when no packages visible in table', () {
+      final results = VerificationResults()
+        ..addResult(Result.info(pkgA, Result.alreadyPublishedMessage,
+            isAffected: false))
+        ..addResult(Result.info(pkgB, Result.wipMessage, isAffected: false));
+
+      expect(results.visibleResults, isEmpty);
+      expect(results.hiddenResults.length, 2);
+
+      final markdown = results.describeAsMarkdown(withTag: false);
+      expect(
+        markdown,
+        equals('* 1 already published.\n* 1 WIP (no publish necessary).'),
+      );
+    });
+  });
+
+  group('Firehose.verify with LocalGithubApi', () {
+    test('filters packages based on PR file changes', () async {
+      final testDir = Directory('test_data/test_repo');
+      final firehose = Firehose(testDir, false, []);
+      final github = LocalGithubApi(
+        prLabels: [],
+        files: [
+          GitFile(
+            'pkgs/package1/lib/package1.dart',
+            FileStatus.modified,
+            testDir,
+          ),
+        ],
+      );
+
+      final results = await firehose.verify(github);
+      final affectedResults =
+          results.results.where((r) => r.isAffected).toList();
+      final unaffectedResults =
+          results.results.where((r) => !r.isAffected).toList();
+
+      expect(affectedResults.map((r) => r.package.name), ['package1']);
+      expect(
+        unaffectedResults.map((r) => r.package.name),
+        containsAll(['package2', 'package3', 'package4', 'package5']),
+      );
+    });
+  });
+}