From fb012e614b2e6fe414406a790827247541c1b33a Mon Sep 17 00:00:00 2001 From: hangyu Date: Mon, 17 Nov 2025 15:10:49 -0800 Subject: [PATCH] lint lint update tests Update publish_command_test.dart Update publish_command_test.dart 2 2 Update publish_command_test.dart lint Update publish_command.dart Update release_from_batch_release.yml Update release.yml clean up code Update publish_command.dart 1 --- .github/workflows/release.yml | 16 +- .github/workflows/release_from_branches.yml | 14 ++ .github/workflows/release_from_main.yml | 12 ++ script/tool/lib/src/publish_command.dart | 38 ++++ script/tool/test/publish_command_test.dart | 220 ++++++++++++++++++++ script/tool/test/util.dart | 16 ++ 6 files changed, 311 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release_from_branches.yml create mode 100644 .github/workflows/release_from_main.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 835b40c18e3..36505038c53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,14 @@ -name: release +name: Reusable Release + on: - push: - branches: - - main + workflow_call: + inputs: + publish-args: + required: true + type: string + workflow-name: + required: true + type: string # Declare default permissions as read only. permissions: read-all @@ -82,5 +88,5 @@ jobs: run: | git config --global user.name ${{ secrets.USER_NAME }} git config --global user.email ${{ secrets.USER_EMAIL }} - dart ./script/tool/lib/src/main.dart publish --all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin + dart ./script/tool/lib/src/main.dart publish ${{ inputs.publish-args }} env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"} diff --git a/.github/workflows/release_from_branches.yml b/.github/workflows/release_from_branches.yml new file mode 100644 index 00000000000..ff07b67ddc6 --- /dev/null +++ b/.github/workflows/release_from_branches.yml @@ -0,0 +1,14 @@ +name: Batch Release +on: + push: + branches: + - 'release-go-router' + - 'release-material' + - 'release-cupertino' +jobs: + release: + uses: ./.github/workflows/release.yml + with: + publish-args: '--all-changed --batch-release --base-sha=HEAD~ --skip-confirmation --remote=origin' + workflow-name: 'Batch Release ${{ github.ref_name }}' + secrets: inherit diff --git a/.github/workflows/release_from_main.yml b/.github/workflows/release_from_main.yml new file mode 100644 index 00000000000..d639bea607e --- /dev/null +++ b/.github/workflows/release_from_main.yml @@ -0,0 +1,12 @@ +name: Main Release +on: + push: + branches: + - main +jobs: + release: + uses: ./.github/workflows/release.yml + with: + publish-args: '--all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin' + workflow-name: 'Main Release' + secrets: inherit diff --git a/script/tool/lib/src/publish_command.dart b/script/tool/lib/src/publish_command.dart index 4d9da43ddc4..8e0c037ab32 100644 --- a/script/tool/lib/src/publish_command.dart +++ b/script/tool/lib/src/publish_command.dart @@ -83,6 +83,10 @@ class PublishCommand extends PackageLoopingCommand { 'Release all packages that contains pubspec changes at the current commit compares to the base-sha.\n' 'The --packages option is ignored if this is on.', ); + argParser.addFlag( + _batchReleaseFlag, + help: 'only release the packages that opt-in for batch release option.', + ); argParser.addFlag( _dryRunFlag, help: @@ -109,6 +113,7 @@ class PublishCommand extends PackageLoopingCommand { static const String _pubFlagsOption = 'pub-publish-flags'; static const String _remoteOption = 'remote'; static const String _allChangedFlag = 'all-changed'; + static const String _batchReleaseFlag = 'batch-release'; static const String _dryRunFlag = 'dry-run'; static const String _skipConfirmationFlag = 'skip-confirmation'; static const String _tagForAutoPublishFlag = 'tag-for-auto-publish'; @@ -196,6 +201,39 @@ class PublishCommand extends PackageLoopingCommand { .toList(); for (final pubspecPath in changedPubspecs) { + // Read the ci_config.yaml file if it exists + + final String packageName = p.basename(p.dirname(pubspecPath)); + bool isBatchReleasePackage; + try { + final File ciConfigFile = packagesDir.fileSystem + .file(pubspecPath) + .parent + .childFile('ci_config.yaml'); + if (!ciConfigFile.existsSync()) { + isBatchReleasePackage = false; + } else { + final ciConfig = + loadYaml(ciConfigFile.readAsStringSync()) as YamlMap?; + final dynamic batchValue = + (ciConfig?['release'] as YamlMap?)?['batch']; + if (batchValue is! bool) { + printError( + '`release.batch` key is missing or not a boolean in ci_config.yaml for $packageName.', + ); + continue; + } + isBatchReleasePackage = batchValue; + } + } catch (e) { + printError('Could not parse ci_config.yaml for $packageName: $e'); + continue; + } + // Skip the package if batch release flag is not set to match the ci_config.yaml + if (getBoolArg(_batchReleaseFlag) != isBatchReleasePackage) { + continue; + } + // git outputs a relativa, Posix-style path. final File pubspecFile = childFileWithSubcomponents( packagesDir.fileSystem.directory((await gitDir).path), diff --git a/script/tool/test/publish_command_test.dart b/script/tool/test/publish_command_test.dart index f106f034956..8c9a0d163d2 100644 --- a/script/tool/test/publish_command_test.dart +++ b/script/tool/test/publish_command_test.dart @@ -1284,6 +1284,226 @@ void main() { ); }); + group('--batch-release flag', () { + test( + 'filters packages based on the existence of ci_config.yaml', + () async { + // Mock pub.dev responses. + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': ['0.0.1'], + }; + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': ['0.0.1'], + }; + + // Mock packages. + final RepositoryPackage plugin1 = createFakePlugin( + 'plugin1', + packagesDir, + version: '0.0.2', + batchRelease: true, + ); + + final RepositoryPackage plugin2 = createFakePlugin( + 'plugin2', + packagesDir, + version: '0.0.2', + ); + + expect(plugin1.ciConfigFile.existsSync(), true); + expect(plugin2.ciConfigFile.existsSync(), false); + + // Mock git diff to show both packages have changed. + processRunner + .mockProcessesForExecutable['git-diff'] = [ + FakeProcessInfo( + MockProcess( + stdout: + '${plugin1.pubspecFile.path}\n${plugin2.pubspecFile.path}', + ), + ), + ]; + + mockStdin.readLineOutput = 'y'; + + final List output = await runCapturingPrint( + commandRunner, + [ + 'publish', + '--all-changed', + '--base-sha=HEAD~', + '--batch-release', + ], + ); + // Package1 is published in batch realease, pacakge2 is not. + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in ${plugin1.path}...'), + contains('Published plugin1 successfully!'), + ]), + ); + + expect( + output, + isNot( + contains( + contains('Running `pub publish ` in ${plugin2.path}...!'), + ), + ), + ); + expect( + output, + isNot(contains(contains('Published plugin2 successfully!'))), + ); + }, + ); + + test( + 'filters packages based on the batch release flag value in ci_config.yaml', + () async { + // Mock pub.dev responses. + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': ['0.0.1'], + }; + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': ['0.0.1'], + }; + + // Mock packages. + final RepositoryPackage plugin1 = createFakePlugin( + 'plugin1', + packagesDir, + version: '0.0.2', + batchRelease: true, + ); + + final RepositoryPackage plugin2 = createFakePlugin( + 'plugin2', + packagesDir, + version: '0.0.2', + batchRelease: false, + ); + + // Mock git diff to show both packages have changed. + processRunner + .mockProcessesForExecutable['git-diff'] = [ + FakeProcessInfo( + MockProcess( + stdout: + '${plugin1.pubspecFile.path}\n${plugin2.pubspecFile.path}', + ), + ), + ]; + + mockStdin.readLineOutput = 'y'; + + final List output = await runCapturingPrint( + commandRunner, + [ + 'publish', + '--all-changed', + '--base-sha=HEAD~', + '--batch-release', + ], + ); + // Package1 is published in batch realease, pacakge2 is not. + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in ${plugin1.path}...'), + contains('Published plugin1 successfully!'), + ]), + ); + + expect( + output, + isNot( + contains( + contains('Running `pub publish ` in ${plugin2.path}...!'), + ), + ), + ); + expect( + output, + isNot(contains(contains('Published plugin2 successfully!'))), + ); + }, + ); + + test( + 'when --batch-release flag value is false, batch release packages are filtered out', + () async { + // Mock pub.dev responses. + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': ['0.0.1'], + }; + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': ['0.0.1'], + }; + + // Mock packages. + final RepositoryPackage plugin1 = createFakePlugin( + 'plugin1', + packagesDir, + version: '0.0.2', + ); + + final RepositoryPackage plugin2 = createFakePlugin( + 'plugin2', + packagesDir, + version: '0.0.2', + batchRelease: true, + ); + + // Mock git diff to show both packages have changed. + processRunner + .mockProcessesForExecutable['git-diff'] = [ + FakeProcessInfo( + MockProcess( + stdout: + '${plugin1.pubspecFile.path}\n${plugin2.pubspecFile.path}', + ), + ), + ]; + + mockStdin.readLineOutput = 'y'; + + final List output = await runCapturingPrint( + commandRunner, + ['publish', '--all-changed', '--base-sha=HEAD~'], + ); + // Package1 is published in batch realease, pacakge2 is not. + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in ${plugin1.path}...'), + contains('Published plugin1 successfully!'), + ]), + ); + + expect( + output, + isNot( + contains( + contains('Running `pub publish ` in ${plugin2.path}...!'), + ), + ), + ); + expect( + output, + isNot(contains(contains('Published plugin2 successfully!'))), + ); + }, + ); + }); + test('Do not release flutter_plugin_tools', () async { mockHttpResponses['plugin1'] = { 'name': 'flutter_plugin_tools', diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 493e4641309..defd7e844b1 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -96,6 +96,7 @@ RepositoryPackage createFakePlugin( String? version = '0.0.1', String flutterConstraint = _defaultFlutterConstraint, String dartConstraint = _defaultDartConstraint, + bool? batchRelease, }) { final RepositoryPackage package = createFakePackage( name, @@ -117,6 +118,9 @@ RepositoryPackage createFakePlugin( flutterConstraint: flutterConstraint, dartConstraint: dartConstraint, ); + if (batchRelease != null) { + createFakeCiConfig(batchRelease, package); + } return package; } @@ -287,6 +291,18 @@ $pluginSection package.pubspecFile.writeAsStringSync(yaml); } +/// Creates a `ci_config.yaml` file for [package]. +void createFakeCiConfig(bool batchRelease, RepositoryPackage package) { + final yaml = + ''' +release: + batch: $batchRelease +'''; + + package.ciConfigFile.createSync(); + package.ciConfigFile.writeAsStringSync(yaml); +} + String _pluginPlatformSection( String platform, PlatformDetails support,