From 152d975ad4ced723a8074f279b3be0d3fe953faa Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Thu, 22 Jan 2026 15:16:41 -0800 Subject: [PATCH 01/16] [ci] Adds more ci check for batch release file integrity --- packages/go_router/ci_config.yaml | 3 +- .../src/repo_package_info_check_command.dart | 186 +++++++- .../repo_package_info_check_command_test.dart | 431 +++++++++++++++++- 3 files changed, 615 insertions(+), 5 deletions(-) diff --git a/packages/go_router/ci_config.yaml b/packages/go_router/ci_config.yaml index f7160fb4571b..5784c6eb1d56 100644 --- a/packages/go_router/ci_config.yaml +++ b/packages/go_router/ci_config.yaml @@ -1,3 +1,2 @@ release: - # TODO(chunhtai): Opt in when ready. - batch: false + batch: true diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 23af0030ee27..3a934382d7df 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -2,7 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' as io; + import 'package:file/file.dart'; +import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/output_utils.dart'; @@ -16,7 +19,11 @@ const int _exitUnknownPackageEntry = 4; /// repo README and CODEOWNERS entries. class RepoPackageInfoCheckCommand extends PackageLoopingCommand { /// Creates Dependabot check command instance. - RepoPackageInfoCheckCommand(super.packagesDir, {super.gitDir}); + RepoPackageInfoCheckCommand( + super.packagesDir, { + super.processRunner, + super.gitDir, + }); late Directory _repoRoot; @@ -149,6 +156,8 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { errors.add(e.message); } + errors.addAll(await _validateFilesBasedOnReleaseStrategy(package)); + // All published packages should have a README.md entry. if (package.isPublishable()) { errors.addAll(_validateRootReadme(package)); @@ -264,4 +273,179 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { return 'p: $packageName'; } } + + Future> _validateFilesBasedOnReleaseStrategy( + RepositoryPackage package, + ) async { + final errors = []; + final bool isBatchRelease = + package.parseCIConfig()?.isBatchRelease ?? false; + final String packageName = package.directory.basename; + final Directory workflowDir = _repoRoot + .childDirectory('.github') + .childDirectory('workflows'); + + // 1. Verify specific batch workflow file. + final File batchWorkflowFile = workflowDir.childFile( + '${packageName}_batch.yml', + ); + if (isBatchRelease) { + if (!batchWorkflowFile.existsSync()) { + errors.add( + 'Missing batch workflow file: .github/workflows/${packageName}_batch.yml', + ); + } else { + // Validate content. + final String content = batchWorkflowFile.readAsStringSync(); + YamlMap? yaml; + try { + yaml = loadYaml(content) as YamlMap?; + } catch (e) { + errors.add('Invalid YAML in ${packageName}_batch.yml: $e'); + } + + if (yaml != null) { + var foundDispatch = false; + final jobs = yaml['jobs'] as YamlMap?; + if (jobs != null) { + for (final Object? job in jobs.values) { + if (job is YamlMap && job['steps'] is YamlList) { + final steps = job['steps'] as YamlList; + for (final Object? step in steps) { + if (step is YamlMap && + step['uses'] is String && + (step['uses'] as String).startsWith( + 'peter-evans/repository-dispatch', + )) { + final withArgs = step['with'] as YamlMap?; + if (withArgs != null && + withArgs['event-type'] == 'batch-release-pr' && + withArgs['client-payload'] == + '{"package": "$packageName"}') { + foundDispatch = true; + } + } + } + } + } + } + + if (!foundDispatch) { + errors.add( + 'Invalid batch workflow content in ${packageName}_batch.yml. ' + 'Must contain a step using peter-evans/repository-dispatch with:\n' + ' event-type: batch-release-pr\n' + ' client-payload: \'{"package": "$packageName"}\'', + ); + } + } + } + } else { + if (batchWorkflowFile.existsSync()) { + errors.add( + 'Unexpected batch workflow file: .github/workflows/${packageName}_batch.yml', + ); + } + } + + // 2. Verify both release_from_branches.yml and sync_release_pr.yml + // have the correct trigger for batch release packages. + errors.addAll( + _validateGlobalWorkflowTrigger( + 'release_from_branches.yml', + workflowDir: workflowDir, + isBatchRelease: isBatchRelease, + packageName: packageName, + ), + ); + errors.addAll( + _validateGlobalWorkflowTrigger( + 'sync_release_pr.yml', + workflowDir: workflowDir, + isBatchRelease: isBatchRelease, + packageName: packageName, + ), + ); + + // 3. Verify remote branch exists. + final io.ProcessResult result = await (await gitDir).runCommand([ + 'show-ref', + '--verify', + '--quiet', + 'refs/heads/release-$packageName', + ], throwOnError: false); + final branchExists = result.exitCode == 0; + if (isBatchRelease && !branchExists) { + errors.add('Branch release-$packageName does not exist on remote origin'); + } else if (!isBatchRelease && branchExists) { + errors.add('Unexpected branch release-$packageName on remote origin'); + } + + // 4. Verify GitHub label exists. + // Using gh CLI. + try { + final io.ProcessResult result = await processRunner.run('gh', [ + 'label', + 'view', + 'post-release-$packageName', + '--repo', + 'flutter/packages', + ]); + final labelExists = result.exitCode == 0; + if (isBatchRelease && !labelExists) { + errors.add( + 'Label post-release-$packageName does not exist in flutter/packages', + ); + } else if (!isBatchRelease && labelExists) { + errors.add( + 'Unexpected label post-release-$packageName in flutter/packages', + ); + } + } catch (e) { + // gh might not be installed. + // We can check if it was a "command not found" error, but ProcessRunner usually wraps things. + // If we can't run gh, we skip this check silently or with a warning logged to console (not error list). + print( + 'Warning: Skipping label check for $packageName because `gh` command failed or is missing.', + ); + } + + if (errors.isNotEmpty) { + for (final error in errors) { + printError('$indentation$error'); + } + } + + return errors; + } + + List _validateGlobalWorkflowTrigger( + String workflowName, { + required Directory workflowDir, + required bool isBatchRelease, + required String packageName, + }) { + final errors = []; + final File workflowFile = workflowDir.childFile(workflowName); + if (!workflowFile.existsSync()) { + if (isBatchRelease) { + errors.add( + 'Missing global workflow file: .github/workflows/$workflowName', + ); + } + return errors; + } + final String content = workflowFile.readAsStringSync(); + final bool hasTrigger = content.contains("- 'release-$packageName'"); + if (isBatchRelease && !hasTrigger) { + errors.add( + 'Missing trigger for release-$packageName in .github/workflows/$workflowName', + ); + } else if (!isBatchRelease && hasTrigger) { + errors.add( + 'Unexpected trigger for release-$packageName in .github/workflows/$workflowName', + ); + } + return errors; + } } diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index e82e9469f243..81e82595f6f9 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -9,25 +9,40 @@ import 'package:flutter_plugin_tools/src/repo_package_info_check_command.dart'; import 'package:git/git.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { late CommandRunner runner; late Directory root; late Directory packagesDir; + late RecordingProcessRunner processRunner; + late RecordingProcessRunner gitProcessRunner; setUp(() { final GitDir gitDir; - (:packagesDir, processRunner: _, gitProcessRunner: _, :gitDir) = + (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) = configureBaseCommandMocks(); root = packagesDir.fileSystem.currentDirectory; - final command = RepoPackageInfoCheckCommand(packagesDir, gitDir: gitDir); + final command = RepoPackageInfoCheckCommand( + packagesDir, + processRunner: processRunner, + gitDir: gitDir, + ); runner = CommandRunner( 'dependabot_test', 'Test for $RepoPackageInfoCheckCommand', ); runner.addCommand(command); + + // Default to failing these checks so that tests of non-batch-release packages + // (the default) don't fail due to "unexpected" branches/labels being found. + gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + [FakeProcessInfo(MockProcess(exitCode: 1))]; + processRunner.mockProcessesForExecutable['gh'] = [ + FakeProcessInfo(MockProcess(exitCode: 1)), + ]; }); String readmeTableHeader() { @@ -121,9 +136,24 @@ ${readmeTableEntry('a_package')} ${readmeTableHeader()} ${readmeTableEntry(pluginName)} '''); + writeAutoLabelerYaml([packages.first]); writeAutoLabelerYaml([packages.first]); writeCodeOwners(packages); + // 4 packages * 2 checks (git, gh) = 8 calls. + // Default mocks in setUp cover 1 call each. We need 3 more each. + gitProcessRunner.mockProcessesForExecutable['git-ls-remote']! + .addAll([ + FakeProcessInfo(MockProcess(exitCode: 1)), + FakeProcessInfo(MockProcess(exitCode: 1)), + FakeProcessInfo(MockProcess(exitCode: 1)), + ]); + processRunner.mockProcessesForExecutable['gh']!.addAll([ + FakeProcessInfo(MockProcess(exitCode: 1)), + FakeProcessInfo(MockProcess(exitCode: 1)), + FakeProcessInfo(MockProcess(exitCode: 1)), + ]); + final List output = await runCapturingPrint(runner, [ 'repo-package-info-check', ]); @@ -669,4 +699,401 @@ release: ); }); }); + + group('release strategy check', () { + RepositoryPackage setupReleaseStrategyTest() { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + ); + + root.childFile('README.md').writeAsStringSync(''' +${readmeTableHeader()} +${readmeTableEntry('a_package')} +'''); + writeAutoLabelerYaml([package]); + writeCodeOwners([package]); + return package; + } + + void writeBatchConfig(RepositoryPackage package) { + package.ciConfigFile.writeAsStringSync(''' +release: + batch: true +'''); + } + + void writeWorkflowFiles({ + bool validBatchFile = true, + bool validReleaseFromBranches = true, + bool validSyncRelease = true, + }) { + final Directory workflowDir = root + .childDirectory('.github') + .childDirectory('workflows'); + workflowDir.createSync(recursive: true); + + if (validBatchFile) { + workflowDir.childFile('a_package_batch.yml').writeAsStringSync(''' +name: Batch Release +on: + schedule: + - cron: "0 8 * * 1" +jobs: + dispatch_release_pr: + runs-on: ubuntu-latest + steps: + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f + with: + event-type: batch-release-pr + client-payload: '{"package": "a_package"}' +'''); + } + + if (validReleaseFromBranches) { + workflowDir.childFile('release_from_branches.yml').writeAsStringSync(''' +on: + push: + branches: + - 'release-a_package' +'''); + } + + if (validSyncRelease) { + workflowDir.childFile('sync_release_pr.yml').writeAsStringSync(''' +on: + push: + branches: + - 'release-a_package' +'''); + } + } + + test( + 'ignores non-batch release packages if they have no artifacts', + () async { + setupReleaseStrategyTest(); + // No config, so batch is false by default. + + gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + [ + FakeProcessInfo( + MockProcess(exitCode: 1), + ), // git ls-remote fails (branch doesn't exist) + ]; + processRunner.mockProcessesForExecutable['gh'] = [ + FakeProcessInfo( + MockProcess(exitCode: 1), + ), // gh label view fails (label doesn't exist) + ]; + + final List output = await runCapturingPrint(runner, [ + 'repo-package-info-check', + ]); + + expect( + output, + containsAllInOrder([contains('No issues found!')]), + ); + }, + ); + + test('fails if non-batch package has batch artifacts', () async { + setupReleaseStrategyTest(); + // batch defaults to false + writeWorkflowFiles(); // Writes batch artifacts + + gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + [ + FakeProcessInfo( + MockProcess(exitCode: 0), + ), // git ls-remote succeeds (branch exists) + ]; + processRunner.mockProcessesForExecutable['gh'] = [ + FakeProcessInfo( + MockProcess(exitCode: 0), + ), // gh label view succeeds (label exists) + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['repo-package-info-check'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + contains( + contains( + 'Unexpected batch workflow file: .github/workflows/a_package_batch.yml', + ), + ), + ); + expect( + output, + contains( + contains( + 'Unexpected trigger for release-a_package in .github/workflows/release_from_branches.yml', + ), + ), + ); + expect( + output, + contains( + contains( + 'Unexpected trigger for release-a_package in .github/workflows/sync_release_pr.yml', + ), + ), + ); + expect( + output, + contains( + contains('Unexpected branch release-a_package on remote origin'), + ), + ); + expect( + output, + contains( + contains( + 'Unexpected label post-release-a_package in flutter/packages', + ), + ), + ); + }); + + test('fails if batch workflow file is missing', () async { + final RepositoryPackage package = setupReleaseStrategyTest(); + writeBatchConfig(package); + // Don't write workflow files. + // Need to write global workflow files to avoid those errors masking the one we test? + // Actually, if batch workflow is missing, that's an error on its own. + // But we probably want to isolate it. + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['repo-package-info-check'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + contains( + contains( + 'Missing batch workflow file: .github/workflows/a_package_batch.yml', + ), + ), + ); + }); + + test('fails if batch workflow content is invalid', () async { + final RepositoryPackage package = setupReleaseStrategyTest(); + writeBatchConfig(package); + final Directory workflowDir = root + .childDirectory('.github') + .childDirectory('workflows'); + workflowDir.createSync(recursive: true); + workflowDir.childFile('a_package_batch.yml').writeAsStringSync(''' +name: Batch Release +jobs: + dispatch_release_pr: + steps: + - uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f + with: + event-type: something-else + client-payload: '{"package": "a_package"}' +'''); + // Write other files to be valid so we focus on this error + workflowDir + .childFile('release_from_branches.yml') + .writeAsStringSync("- 'release-a_package'"); + workflowDir + .childFile('sync_release_pr.yml') + .writeAsStringSync("- 'release-a_package'"); + + // Mock successful git and gh calls + gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + [FakeProcessInfo(MockProcess())]; + processRunner.mockProcessesForExecutable['gh'] = [ + FakeProcessInfo(MockProcess()), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['repo-package-info-check'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + contains( + contains( + 'Invalid batch workflow content in a_package_batch.yml. ' + 'Must contain a step using peter-evans/repository-dispatch with:\n' + ' event-type: batch-release-pr\n' + ' client-payload: \'{"package": "a_package"}\'', + ), + ), + ); + }); + + test('fails if global workflows are missing triggers', () async { + final RepositoryPackage package = setupReleaseStrategyTest(); + writeBatchConfig(package); + writeWorkflowFiles( + validReleaseFromBranches: false, + validSyncRelease: false, + ); + // Create files but without correct content + final Directory workflowDir = root + .childDirectory('.github') + .childDirectory('workflows'); + workflowDir + .childFile('release_from_branches.yml') + .writeAsStringSync('something'); + workflowDir + .childFile('sync_release_pr.yml') + .writeAsStringSync('something'); + + processRunner.mockProcessesForExecutable['git'] = [ + FakeProcessInfo(MockProcess()), + ]; + processRunner.mockProcessesForExecutable['gh'] = [ + FakeProcessInfo(MockProcess()), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['repo-package-info-check'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + contains( + contains( + 'Missing trigger for release-a_package in .github/workflows/release_from_branches.yml', + ), + ), + ); + expect( + output, + contains( + contains( + 'Missing trigger for release-a_package in .github/workflows/sync_release_pr.yml', + ), + ), + ); + }); + + test('fails if remote branch check fails', () async { + final RepositoryPackage package = setupReleaseStrategyTest(); + writeBatchConfig(package); + writeWorkflowFiles(); + + gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + [ + FakeProcessInfo(MockProcess(exitCode: 1)), // git ls-remote fails + ]; + processRunner.mockProcessesForExecutable['gh'] = [ + FakeProcessInfo(MockProcess()), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['repo-package-info-check'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + contains( + contains('Branch release-a_package does not exist on remote origin'), + ), + ); + }); + + test('fails if label check fails', () async { + final RepositoryPackage package = setupReleaseStrategyTest(); + writeBatchConfig(package); + writeWorkflowFiles(); + + gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + [FakeProcessInfo(MockProcess())]; + processRunner.mockProcessesForExecutable['gh'] = [ + FakeProcessInfo(MockProcess(exitCode: 1)), // gh label view fails + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['repo-package-info-check'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + contains( + contains( + 'Label post-release-a_package does not exist in flutter/packages', + ), + ), + ); + }); + + test('passes if all checks pass', () async { + final RepositoryPackage package = setupReleaseStrategyTest(); + writeBatchConfig(package); + writeWorkflowFiles(); + + gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + [FakeProcessInfo(MockProcess())]; + processRunner.mockProcessesForExecutable['gh'] = [ + FakeProcessInfo(MockProcess()), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['repo-package-info-check'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + if (commandError != null) { + print('ERROR: Command failed in "passes if all checks pass"'); + print('Output:\n${output.join('\n')}'); + } + + expect(commandError, isNull); + expect( + output, + containsAllInOrder([contains('No issues found!')]), + ); + }); + }); } From 08e2ef72a8eeaf56993dbe86c122a29cbd7c62a4 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Thu, 22 Jan 2026 15:37:36 -0800 Subject: [PATCH 02/16] update --- script/tool/test/repo_package_info_check_command_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index 81e82595f6f9..1c187337cce4 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -807,12 +807,12 @@ on: gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = [ FakeProcessInfo( - MockProcess(exitCode: 0), + MockProcess(), ), // git ls-remote succeeds (branch exists) ]; processRunner.mockProcessesForExecutable['gh'] = [ FakeProcessInfo( - MockProcess(exitCode: 0), + MockProcess(), ), // gh label view succeeds (label exists) ]; From 5a8ecb2ff6de3da6e1dfc6a40ce7e4a6e507be74 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Thu, 22 Jan 2026 15:47:01 -0800 Subject: [PATCH 03/16] fix test --- script/tool/test/repo_package_info_check_command_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index 1c187337cce4..824b8fad6992 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -142,7 +142,7 @@ ${readmeTableEntry(pluginName)} // 4 packages * 2 checks (git, gh) = 8 calls. // Default mocks in setUp cover 1 call each. We need 3 more each. - gitProcessRunner.mockProcessesForExecutable['git-ls-remote']! + gitProcessRunner.mockProcessesForExecutable['git-show-ref']! .addAll([ FakeProcessInfo(MockProcess(exitCode: 1)), FakeProcessInfo(MockProcess(exitCode: 1)), From 5cd273bc50d9c964488ae6b8645c8d9c76af0a8a Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 23 Jan 2026 09:53:36 -0800 Subject: [PATCH 04/16] update --- script/tool/lib/src/repo_package_info_check_command.dart | 1 + script/tool/test/repo_package_info_check_command_test.dart | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 3a934382d7df..47474188aead 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -375,6 +375,7 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { 'refs/heads/release-$packageName', ], throwOnError: false); final branchExists = result.exitCode == 0; + print('result: ${result.stdout}, error: ${result.stderr}'); if (isBatchRelease && !branchExists) { errors.add('Branch release-$packageName does not exist on remote origin'); } else if (!isBatchRelease && branchExists) { diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index 824b8fad6992..f0da51419038 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -811,9 +811,7 @@ on: ), // git ls-remote succeeds (branch exists) ]; processRunner.mockProcessesForExecutable['gh'] = [ - FakeProcessInfo( - MockProcess(), - ), // gh label view succeeds (label exists) + FakeProcessInfo(MockProcess()), // gh label view succeeds (label exists) ]; Error? commandError; From 6b2ccdcfc0de16b88977b188ecf177c1156a9c4c Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 23 Jan 2026 10:21:23 -0800 Subject: [PATCH 05/16] update --- script/tool/lib/src/repo_package_info_check_command.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 47474188aead..1ab06ad4d1d3 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -371,7 +371,6 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { final io.ProcessResult result = await (await gitDir).runCommand([ 'show-ref', '--verify', - '--quiet', 'refs/heads/release-$packageName', ], throwOnError: false); final branchExists = result.exitCode == 0; From 9662e2d869887d2eaff62cc71bb677562c8308f1 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 23 Jan 2026 11:12:29 -0800 Subject: [PATCH 06/16] update --- .../src/repo_package_info_check_command.dart | 40 ++-------- .../repo_package_info_check_command_test.dart | 78 +------------------ 2 files changed, 9 insertions(+), 109 deletions(-) diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 1ab06ad4d1d3..bad916716f8f 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -19,11 +19,7 @@ const int _exitUnknownPackageEntry = 4; /// repo README and CODEOWNERS entries. class RepoPackageInfoCheckCommand extends PackageLoopingCommand { /// Creates Dependabot check command instance. - RepoPackageInfoCheckCommand( - super.packagesDir, { - super.processRunner, - super.gitDir, - }); + RepoPackageInfoCheckCommand(super.packagesDir, {super.gitDir}); late Directory _repoRoot; @@ -375,41 +371,17 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { ], throwOnError: false); final branchExists = result.exitCode == 0; print('result: ${result.stdout}, error: ${result.stderr}'); + final io.ProcessResult result2 = await (await gitDir).runCommand([ + 'remote', + '-v', + ], throwOnError: false); + print('result2: ${result2.stdout}, error: ${result2.stderr}'); if (isBatchRelease && !branchExists) { errors.add('Branch release-$packageName does not exist on remote origin'); } else if (!isBatchRelease && branchExists) { errors.add('Unexpected branch release-$packageName on remote origin'); } - // 4. Verify GitHub label exists. - // Using gh CLI. - try { - final io.ProcessResult result = await processRunner.run('gh', [ - 'label', - 'view', - 'post-release-$packageName', - '--repo', - 'flutter/packages', - ]); - final labelExists = result.exitCode == 0; - if (isBatchRelease && !labelExists) { - errors.add( - 'Label post-release-$packageName does not exist in flutter/packages', - ); - } else if (!isBatchRelease && labelExists) { - errors.add( - 'Unexpected label post-release-$packageName in flutter/packages', - ); - } - } catch (e) { - // gh might not be installed. - // We can check if it was a "command not found" error, but ProcessRunner usually wraps things. - // If we can't run gh, we skip this check silently or with a warning logged to console (not error list). - print( - 'Warning: Skipping label check for $packageName because `gh` command failed or is missing.', - ); - } - if (errors.isNotEmpty) { for (final error in errors) { printError('$indentation$error'); diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index f0da51419038..da61aad6d99c 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -16,20 +16,15 @@ void main() { late CommandRunner runner; late Directory root; late Directory packagesDir; - late RecordingProcessRunner processRunner; late RecordingProcessRunner gitProcessRunner; setUp(() { final GitDir gitDir; - (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) = + (:packagesDir, processRunner: _, :gitProcessRunner, :gitDir) = configureBaseCommandMocks(); root = packagesDir.fileSystem.currentDirectory; - final command = RepoPackageInfoCheckCommand( - packagesDir, - processRunner: processRunner, - gitDir: gitDir, - ); + final command = RepoPackageInfoCheckCommand(packagesDir, gitDir: gitDir); runner = CommandRunner( 'dependabot_test', 'Test for $RepoPackageInfoCheckCommand', @@ -40,9 +35,6 @@ void main() { // (the default) don't fail due to "unexpected" branches/labels being found. gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = [FakeProcessInfo(MockProcess(exitCode: 1))]; - processRunner.mockProcessesForExecutable['gh'] = [ - FakeProcessInfo(MockProcess(exitCode: 1)), - ]; }); String readmeTableHeader() { @@ -148,11 +140,6 @@ ${readmeTableEntry(pluginName)} FakeProcessInfo(MockProcess(exitCode: 1)), FakeProcessInfo(MockProcess(exitCode: 1)), ]); - processRunner.mockProcessesForExecutable['gh']!.addAll([ - FakeProcessInfo(MockProcess(exitCode: 1)), - FakeProcessInfo(MockProcess(exitCode: 1)), - FakeProcessInfo(MockProcess(exitCode: 1)), - ]); final List output = await runCapturingPrint(runner, [ 'repo-package-info-check', @@ -782,11 +769,6 @@ on: MockProcess(exitCode: 1), ), // git ls-remote fails (branch doesn't exist) ]; - processRunner.mockProcessesForExecutable['gh'] = [ - FakeProcessInfo( - MockProcess(exitCode: 1), - ), // gh label view fails (label doesn't exist) - ]; final List output = await runCapturingPrint(runner, [ 'repo-package-info-check', @@ -810,9 +792,6 @@ on: MockProcess(), ), // git ls-remote succeeds (branch exists) ]; - processRunner.mockProcessesForExecutable['gh'] = [ - FakeProcessInfo(MockProcess()), // gh label view succeeds (label exists) - ]; Error? commandError; final List output = await runCapturingPrint( @@ -854,14 +833,6 @@ on: contains('Unexpected branch release-a_package on remote origin'), ), ); - expect( - output, - contains( - contains( - 'Unexpected label post-release-a_package in flutter/packages', - ), - ), - ); }); test('fails if batch workflow file is missing', () async { @@ -920,9 +891,6 @@ jobs: // Mock successful git and gh calls gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = [FakeProcessInfo(MockProcess())]; - processRunner.mockProcessesForExecutable['gh'] = [ - FakeProcessInfo(MockProcess()), - ]; Error? commandError; final List output = await runCapturingPrint( @@ -965,10 +933,7 @@ jobs: .childFile('sync_release_pr.yml') .writeAsStringSync('something'); - processRunner.mockProcessesForExecutable['git'] = [ - FakeProcessInfo(MockProcess()), - ]; - processRunner.mockProcessesForExecutable['gh'] = [ + gitProcessRunner.mockProcessesForExecutable['git'] = [ FakeProcessInfo(MockProcess()), ]; @@ -1009,9 +974,6 @@ jobs: [ FakeProcessInfo(MockProcess(exitCode: 1)), // git ls-remote fails ]; - processRunner.mockProcessesForExecutable['gh'] = [ - FakeProcessInfo(MockProcess()), - ]; Error? commandError; final List output = await runCapturingPrint( @@ -1031,37 +993,6 @@ jobs: ); }); - test('fails if label check fails', () async { - final RepositoryPackage package = setupReleaseStrategyTest(); - writeBatchConfig(package); - writeWorkflowFiles(); - - gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = - [FakeProcessInfo(MockProcess())]; - processRunner.mockProcessesForExecutable['gh'] = [ - FakeProcessInfo(MockProcess(exitCode: 1)), // gh label view fails - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - ['repo-package-info-check'], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - contains( - contains( - 'Label post-release-a_package does not exist in flutter/packages', - ), - ), - ); - }); - test('passes if all checks pass', () async { final RepositoryPackage package = setupReleaseStrategyTest(); writeBatchConfig(package); @@ -1069,9 +1000,6 @@ jobs: gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = [FakeProcessInfo(MockProcess())]; - processRunner.mockProcessesForExecutable['gh'] = [ - FakeProcessInfo(MockProcess()), - ]; Error? commandError; final List output = await runCapturingPrint( From 0e214e644ca662a03c3c43d1d8f724eb0c5890f5 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 23 Jan 2026 12:05:54 -0800 Subject: [PATCH 07/16] update --- script/tool/lib/src/repo_package_info_check_command.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index bad916716f8f..18d8f5c5a2ed 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -365,9 +365,10 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { // 3. Verify remote branch exists. final io.ProcessResult result = await (await gitDir).runCommand([ - 'show-ref', - '--verify', - 'refs/heads/release-$packageName', + 'ls-remote', + '--heads', + 'origin', + 'release-$packageName', ], throwOnError: false); final branchExists = result.exitCode == 0; print('result: ${result.stdout}, error: ${result.stderr}'); From 7dfc41ae11a66469e94ee80a1c1eb9a81c982e6f Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 23 Jan 2026 12:26:41 -0800 Subject: [PATCH 08/16] update --- .../tool/lib/src/repo_package_info_check_command.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 18d8f5c5a2ed..73e6d4f7b848 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -366,17 +366,15 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { // 3. Verify remote branch exists. final io.ProcessResult result = await (await gitDir).runCommand([ 'ls-remote', + '--exit-code', '--heads', 'origin', 'release-$packageName', ], throwOnError: false); final branchExists = result.exitCode == 0; - print('result: ${result.stdout}, error: ${result.stderr}'); - final io.ProcessResult result2 = await (await gitDir).runCommand([ - 'remote', - '-v', - ], throwOnError: false); - print('result2: ${result2.stdout}, error: ${result2.stderr}'); + print( + 'result: ${result.stdout}, error: ${result.stderr}, exitCode: ${result.exitCode}', + ); if (isBatchRelease && !branchExists) { errors.add('Branch release-$packageName does not exist on remote origin'); } else if (!isBatchRelease && branchExists) { From cc1b9fc83faf51e0f477aaec1d25c8e7a9ec0e51 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 23 Jan 2026 15:23:36 -0800 Subject: [PATCH 09/16] update --- .../lib/src/repo_package_info_check_command.dart | 3 --- .../test/repo_package_info_check_command_test.dart | 14 +++++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 73e6d4f7b848..d2b20ea00144 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -372,9 +372,6 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { 'release-$packageName', ], throwOnError: false); final branchExists = result.exitCode == 0; - print( - 'result: ${result.stdout}, error: ${result.stderr}, exitCode: ${result.exitCode}', - ); if (isBatchRelease && !branchExists) { errors.add('Branch release-$packageName does not exist on remote origin'); } else if (!isBatchRelease && branchExists) { diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index da61aad6d99c..da5678687908 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -33,7 +33,7 @@ void main() { // Default to failing these checks so that tests of non-batch-release packages // (the default) don't fail due to "unexpected" branches/labels being found. - gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + gitProcessRunner.mockProcessesForExecutable['git-ls-remote'] = [FakeProcessInfo(MockProcess(exitCode: 1))]; }); @@ -134,7 +134,7 @@ ${readmeTableEntry(pluginName)} // 4 packages * 2 checks (git, gh) = 8 calls. // Default mocks in setUp cover 1 call each. We need 3 more each. - gitProcessRunner.mockProcessesForExecutable['git-show-ref']! + gitProcessRunner.mockProcessesForExecutable['git-ls-remote']! .addAll([ FakeProcessInfo(MockProcess(exitCode: 1)), FakeProcessInfo(MockProcess(exitCode: 1)), @@ -763,7 +763,7 @@ on: setupReleaseStrategyTest(); // No config, so batch is false by default. - gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + gitProcessRunner.mockProcessesForExecutable['git-ls-remote'] = [ FakeProcessInfo( MockProcess(exitCode: 1), @@ -786,7 +786,7 @@ on: // batch defaults to false writeWorkflowFiles(); // Writes batch artifacts - gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + gitProcessRunner.mockProcessesForExecutable['git-ls-remote'] = [ FakeProcessInfo( MockProcess(), @@ -889,7 +889,7 @@ jobs: .writeAsStringSync("- 'release-a_package'"); // Mock successful git and gh calls - gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + gitProcessRunner.mockProcessesForExecutable['git-ls-remote'] = [FakeProcessInfo(MockProcess())]; Error? commandError; @@ -970,7 +970,7 @@ jobs: writeBatchConfig(package); writeWorkflowFiles(); - gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + gitProcessRunner.mockProcessesForExecutable['git-ls-remote'] = [ FakeProcessInfo(MockProcess(exitCode: 1)), // git ls-remote fails ]; @@ -998,7 +998,7 @@ jobs: writeBatchConfig(package); writeWorkflowFiles(); - gitProcessRunner.mockProcessesForExecutable['git-show-ref'] = + gitProcessRunner.mockProcessesForExecutable['git-ls-remote'] = [FakeProcessInfo(MockProcess())]; Error? commandError; From 5b1bec79e7cf6c54d0543d453071e0d2d3d6d18a Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 23 Jan 2026 15:37:51 -0800 Subject: [PATCH 10/16] update --- .../tool/lib/src/repo_package_info_check_command.dart | 10 +++++++--- .../test/repo_package_info_check_command_test.dart | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index d2b20ea00144..84baaac2c3c9 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -363,7 +363,7 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { ), ); - // 3. Verify remote branch exists. + // 3. Verify release branch exists on remote flutter/packages if it is a batch release package. final io.ProcessResult result = await (await gitDir).runCommand([ 'ls-remote', '--exit-code', @@ -373,9 +373,13 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { ], throwOnError: false); final branchExists = result.exitCode == 0; if (isBatchRelease && !branchExists) { - errors.add('Branch release-$packageName does not exist on remote origin'); + errors.add( + 'Branch release-$packageName does not exist on remote flutter/packages', + ); } else if (!isBatchRelease && branchExists) { - errors.add('Unexpected branch release-$packageName on remote origin'); + errors.add( + 'Unexpected branch release-$packageName on remote flutter/packages', + ); } if (errors.isNotEmpty) { diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index da5678687908..0e52ead93d82 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -830,7 +830,9 @@ on: expect( output, contains( - contains('Unexpected branch release-a_package on remote origin'), + contains( + 'Unexpected branch release-a_package on remote flutter/packages', + ), ), ); }); @@ -988,7 +990,9 @@ jobs: expect( output, contains( - contains('Branch release-a_package does not exist on remote origin'), + contains( + 'Branch release-a_package does not exist on remote flutter/packages', + ), ), ); }); From aebb4e02e992f2cc836f2b7c5a279786a73e3f75 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 23 Jan 2026 16:05:39 -0800 Subject: [PATCH 11/16] update --- packages/go_router/pending_changelogs/convert_batch.yaml | 3 +++ packages/go_router/pending_changelogs/template.yaml | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 packages/go_router/pending_changelogs/convert_batch.yaml create mode 100644 packages/go_router/pending_changelogs/template.yaml diff --git a/packages/go_router/pending_changelogs/convert_batch.yaml b/packages/go_router/pending_changelogs/convert_batch.yaml new file mode 100644 index 000000000000..b9e33cba64e6 --- /dev/null +++ b/packages/go_router/pending_changelogs/convert_batch.yaml @@ -0,0 +1,3 @@ +changelog: | + - Converts go_router to use batch release. +version: patch \ No newline at end of file diff --git a/packages/go_router/pending_changelogs/template.yaml b/packages/go_router/pending_changelogs/template.yaml new file mode 100644 index 000000000000..4873aa2c3050 --- /dev/null +++ b/packages/go_router/pending_changelogs/template.yaml @@ -0,0 +1,6 @@ +# Use this file as template to draft a unreleased changelog file. +# Make a copy of this file in the same directory, rename it, and fill in the details. +changelog: | + - Can include a list of changes. + - with markdown supported. +version: \ No newline at end of file From 60b45a666e95587ef2430eea59222842108860e8 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 23 Jan 2026 16:06:17 -0800 Subject: [PATCH 12/16] new line --- packages/go_router/pending_changelogs/convert_batch.yaml | 2 +- packages/go_router/pending_changelogs/template.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/go_router/pending_changelogs/convert_batch.yaml b/packages/go_router/pending_changelogs/convert_batch.yaml index b9e33cba64e6..1a44027ba4fa 100644 --- a/packages/go_router/pending_changelogs/convert_batch.yaml +++ b/packages/go_router/pending_changelogs/convert_batch.yaml @@ -1,3 +1,3 @@ changelog: | - Converts go_router to use batch release. -version: patch \ No newline at end of file +version: patch diff --git a/packages/go_router/pending_changelogs/template.yaml b/packages/go_router/pending_changelogs/template.yaml index 4873aa2c3050..97107d891a9c 100644 --- a/packages/go_router/pending_changelogs/template.yaml +++ b/packages/go_router/pending_changelogs/template.yaml @@ -3,4 +3,4 @@ changelog: | - Can include a list of changes. - with markdown supported. -version: \ No newline at end of file +version: From cee56b9f2cb6d3689e1789b46a68357c0a4614e7 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Mon, 26 Jan 2026 10:01:56 -0800 Subject: [PATCH 13/16] update --- script/tool/test/repo_package_info_check_command_test.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index 0e52ead93d82..f5df3faff526 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -784,7 +784,7 @@ on: test('fails if non-batch package has batch artifacts', () async { setupReleaseStrategyTest(); // batch defaults to false - writeWorkflowFiles(); // Writes batch artifacts + writeWorkflowFiles(); gitProcessRunner.mockProcessesForExecutable['git-ls-remote'] = [ @@ -841,9 +841,6 @@ on: final RepositoryPackage package = setupReleaseStrategyTest(); writeBatchConfig(package); // Don't write workflow files. - // Need to write global workflow files to avoid those errors masking the one we test? - // Actually, if batch workflow is missing, that's an error on its own. - // But we probably want to isolate it. Error? commandError; final List output = await runCapturingPrint( From dcd190f4794acf1d9b722ca9b9a39eabb4c485b4 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Tue, 27 Jan 2026 15:32:09 -0800 Subject: [PATCH 14/16] fix test --- .../tool/lib/src/repo_package_info_check_command.dart | 10 ++++++---- .../test/repo_package_info_check_command_test.dart | 8 -------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 84baaac2c3c9..7dc26ef39ca8 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -376,11 +376,13 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { errors.add( 'Branch release-$packageName does not exist on remote flutter/packages', ); - } else if (!isBatchRelease && branchExists) { - errors.add( - 'Unexpected branch release-$packageName on remote flutter/packages', - ); } + // Allow branch to exist on remote flutter/packages for non-batch release packages. + // Otherwise, it will be hard to opt package out of batch release. + // + // Enforcing this will run into a deadlock where the ci in the PR to opts out of batch release + // will require removal of the release branch. but removing the release branch will immediately break + // the latest main. if (errors.isNotEmpty) { for (final error in errors) { diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index f5df3faff526..c13fa3c3fed4 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -827,14 +827,6 @@ on: ), ), ); - expect( - output, - contains( - contains( - 'Unexpected branch release-a_package on remote flutter/packages', - ), - ), - ); }); test('fails if batch workflow file is missing', () async { From d30ea14eb2e6de2de7671054de87912b890069b2 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Thu, 29 Jan 2026 14:09:57 -0800 Subject: [PATCH 15/16] addressing comment --- .github/workflows/go_router_batch.yml | 7 +- .../pending_changelogs/convert_batch.yaml | 2 +- .../src/repo_package_info_check_command.dart | 159 ++++++++++++------ 3 files changed, 108 insertions(+), 60 deletions(-) diff --git a/.github/workflows/go_router_batch.yml b/.github/workflows/go_router_batch.yml index 06a8be7d90e9..3687b074108d 100644 --- a/.github/workflows/go_router_batch.yml +++ b/.github/workflows/go_router_batch.yml @@ -2,10 +2,9 @@ name: "Creates Batch Release for go_router" on: workflow_dispatch: - # Uncomment the cron schedule when ready. - # schedule: - # # Run every Monday at 8:00 AM - # - cron: "0 8 * * 1" + schedule: + # Run every Monday at 8:00 AM + - cron: "0 8 * * 1" jobs: dispatch_release_pr: diff --git a/packages/go_router/pending_changelogs/convert_batch.yaml b/packages/go_router/pending_changelogs/convert_batch.yaml index 1a44027ba4fa..1f2c2e52466b 100644 --- a/packages/go_router/pending_changelogs/convert_batch.yaml +++ b/packages/go_router/pending_changelogs/convert_batch.yaml @@ -1,3 +1,3 @@ changelog: | - Converts go_router to use batch release. -version: patch +version: skip diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 7dc26ef39ca8..389efb2e2343 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -281,14 +281,84 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { .childDirectory('.github') .childDirectory('workflows'); - // 1. Verify specific batch workflow file. + errors.addAll( + _validateSpecificBatchWorkflow( + packageName, + workflowDir: workflowDir, + isBatchRelease: isBatchRelease, + ), + ); + + errors.addAll( + _validateGlobalWorkflowTrigger( + 'release_from_branches.yml', + workflowDir: workflowDir, + isBatchRelease: isBatchRelease, + packageName: packageName, + ), + ); + errors.addAll( + _validateGlobalWorkflowTrigger( + 'sync_release_pr.yml', + workflowDir: workflowDir, + isBatchRelease: isBatchRelease, + packageName: packageName, + ), + ); + + errors.addAll( + await _validateRemoteReleaseBranch( + packageName, + isBatchRelease: isBatchRelease, + ), + ); + + return errors; + } + + Future> _validateRemoteReleaseBranch( + String packageName, { + required bool isBatchRelease, + }) async { + final errors = []; + // Verify release branch exists on remote flutter/packages if it is a batch release package. + final io.ProcessResult result = await (await gitDir).runCommand([ + 'ls-remote', + '--exit-code', + '--heads', + 'origin', + 'release-$packageName', + ], throwOnError: false); + final branchExists = result.exitCode == 0; + if (isBatchRelease && !branchExists) { + errors.add( + 'Branch release-$packageName does not exist on remote flutter/packages\n' + 'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release', + ); + } + // Allow branch to exist on remote flutter/packages for non-batch release packages. + // Otherwise, it will be hard to opt package out of batch release. + // + // Enforcing this will run into a deadlock where the ci in the PR to opts out of batch release + // will require removal of the release branch. but removing the release branch will immediately break + // the latest main. + return errors; + } + + List _validateSpecificBatchWorkflow( + String packageName, { + required Directory workflowDir, + required bool isBatchRelease, + }) { + final errors = []; final File batchWorkflowFile = workflowDir.childFile( '${packageName}_batch.yml', ); if (isBatchRelease) { if (!batchWorkflowFile.existsSync()) { errors.add( - 'Missing batch workflow file: .github/workflows/${packageName}_batch.yml', + 'Missing batch workflow file: .github/workflows/${packageName}_batch.yml\n' + 'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release', ); } else { // Validate content. @@ -331,7 +401,8 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { 'Invalid batch workflow content in ${packageName}_batch.yml. ' 'Must contain a step using peter-evans/repository-dispatch with:\n' ' event-type: batch-release-pr\n' - ' client-payload: \'{"package": "$packageName"}\'', + ' client-payload: \'{"package": "$packageName"}\'\n' + 'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release', ); } } @@ -339,57 +410,10 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { } else { if (batchWorkflowFile.existsSync()) { errors.add( - 'Unexpected batch workflow file: .github/workflows/${packageName}_batch.yml', + 'Unexpected batch workflow file: .github/workflows/${packageName}_batch.yml\n', ); } } - - // 2. Verify both release_from_branches.yml and sync_release_pr.yml - // have the correct trigger for batch release packages. - errors.addAll( - _validateGlobalWorkflowTrigger( - 'release_from_branches.yml', - workflowDir: workflowDir, - isBatchRelease: isBatchRelease, - packageName: packageName, - ), - ); - errors.addAll( - _validateGlobalWorkflowTrigger( - 'sync_release_pr.yml', - workflowDir: workflowDir, - isBatchRelease: isBatchRelease, - packageName: packageName, - ), - ); - - // 3. Verify release branch exists on remote flutter/packages if it is a batch release package. - final io.ProcessResult result = await (await gitDir).runCommand([ - 'ls-remote', - '--exit-code', - '--heads', - 'origin', - 'release-$packageName', - ], throwOnError: false); - final branchExists = result.exitCode == 0; - if (isBatchRelease && !branchExists) { - errors.add( - 'Branch release-$packageName does not exist on remote flutter/packages', - ); - } - // Allow branch to exist on remote flutter/packages for non-batch release packages. - // Otherwise, it will be hard to opt package out of batch release. - // - // Enforcing this will run into a deadlock where the ci in the PR to opts out of batch release - // will require removal of the release branch. but removing the release branch will immediately break - // the latest main. - - if (errors.isNotEmpty) { - for (final error in errors) { - printError('$indentation$error'); - } - } - return errors; } @@ -404,20 +428,45 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { if (!workflowFile.existsSync()) { if (isBatchRelease) { errors.add( - 'Missing global workflow file: .github/workflows/$workflowName', + 'Missing global workflow file: .github/workflows/$workflowName\n' + 'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release', ); } return errors; } + final String content = workflowFile.readAsStringSync(); - final bool hasTrigger = content.contains("- 'release-$packageName'"); + YamlMap? yaml; + try { + yaml = loadYaml(content) as YamlMap?; + } catch (e) { + errors.add('Invalid YAML in $workflowName: $e'); + } + + bool hasTrigger = false; + if (yaml != null) { + final on = yaml['on'] as YamlMap?; + if (on is YamlMap) { + final push = on['push'] as YamlMap?; + if (push is YamlMap) { + final branches = push['branches'] as YamlList?; + if (branches is YamlList) { + if (branches.contains('release-$packageName')) { + hasTrigger = true; + } + } + } + } + } + if (isBatchRelease && !hasTrigger) { errors.add( - 'Missing trigger for release-$packageName in .github/workflows/$workflowName', + 'Missing trigger for release-$packageName in .github/workflows/$workflowName\n' + 'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release', ); } else if (!isBatchRelease && hasTrigger) { errors.add( - 'Unexpected trigger for release-$packageName in .github/workflows/$workflowName', + 'Unexpected trigger for release-$packageName in .github/workflows/$workflowName\n', ); } return errors; From 875413e4b26573c60cb9806caffefb05cf2fa58f Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Thu, 29 Jan 2026 14:53:25 -0800 Subject: [PATCH 16/16] update --- script/tool/lib/src/repo_package_info_check_command.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 389efb2e2343..d3ab5d8bdcce 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -443,7 +443,7 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { errors.add('Invalid YAML in $workflowName: $e'); } - bool hasTrigger = false; + var hasTrigger = false; if (yaml != null) { final on = yaml['on'] as YamlMap?; if (on is YamlMap) {