diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart index 0224596f875..1c7071b0b75 100644 --- a/script/tool/lib/src/update_release_info_command.dart +++ b/script/tool/lib/src/update_release_info_command.dart @@ -112,7 +112,15 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - String nextVersionString; + final Version? version = package.parsePubspec().version; + final bool isBatchRelease = + package.parseCIConfig()?.isBatchRelease ?? false; + if (isBatchRelease && (version?.isPreRelease ?? false)) { + return PackageResult.fail([ + 'This command does not support batch releases packages with pre-release versions.', + 'Pre-release version: $version', + ]); + } _VersionIncrementType? versionChange = _versionChange; @@ -144,36 +152,14 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand { } } - if (versionChange != null) { - final Version? updatedVersion = _updatePubspecVersion( + if (isBatchRelease) { + return _createPendingBatchChangelog( package, - versionChange, + versionChange: versionChange, ); - if (updatedVersion == null) { - return PackageResult.fail([ - 'Could not determine current version.', - ]); - } - nextVersionString = updatedVersion.toString(); - print('${indentation}Incremented version to $nextVersionString.'); - } else { - nextVersionString = 'NEXT'; } - final _ChangelogUpdateOutcome updateOutcome = _updateChangelog( - package, - nextVersionString, - ); - switch (updateOutcome) { - case _ChangelogUpdateOutcome.addedSection: - print('${indentation}Added a $nextVersionString section.'); - case _ChangelogUpdateOutcome.updatedSection: - print('${indentation}Updated NEXT section.'); - case _ChangelogUpdateOutcome.failed: - return PackageResult.fail(['Could not update CHANGELOG.md.']); - } - - return PackageResult.success(); + return _updatePubspecAndChangelog(package, versionChange: versionChange); } _ChangelogUpdateOutcome _updateChangelog( @@ -314,4 +300,93 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand { ); } } + + /// Updates the `pubspec.yaml` and `CHANGELOG.md` files directly. + /// + /// This is used for continuous releases, where changes will be released + /// immediately. + Future _updatePubspecAndChangelog( + RepositoryPackage package, { + _VersionIncrementType? versionChange, + }) async { + String nextVersionString; + if (versionChange != null) { + final Version? updatedVersion = _updatePubspecVersion( + package, + versionChange, + ); + if (updatedVersion == null) { + return PackageResult.fail([ + 'Could not determine current version.', + ]); + } + nextVersionString = updatedVersion.toString(); + print('${indentation}Incremented version to $nextVersionString.'); + } else { + nextVersionString = 'NEXT'; + } + + final _ChangelogUpdateOutcome updateOutcome = _updateChangelog( + package, + nextVersionString, + ); + switch (updateOutcome) { + case _ChangelogUpdateOutcome.addedSection: + print('${indentation}Added a $nextVersionString section.'); + case _ChangelogUpdateOutcome.updatedSection: + print('${indentation}Updated NEXT section.'); + case _ChangelogUpdateOutcome.failed: + return PackageResult.fail(['Could not update CHANGELOG.md.']); + } + + return PackageResult.success(); + } + + /// Creates a pending changelog entry in the package's `pending_changelogs` + /// directory. + /// + /// This is used for batch releases, where changes are accumulated in + /// individual files before being merged into the main CHANGELOG.md and + /// pubspec.yaml during the release process. + Future _createPendingBatchChangelog( + RepositoryPackage package, { + _VersionIncrementType? versionChange, + }) async { + final Directory pendingDirectory = package.pendingChangelogsDirectory; + if (!pendingDirectory.existsSync()) { + return PackageResult.fail([ + 'Could not create pending changelog entry. Pending changelog directory does not exist.', + ]); + } + + final VersionChange type; + switch (versionChange) { + case _VersionIncrementType.minor: + type = VersionChange.minor; + case _VersionIncrementType.bugfix: + type = VersionChange.patch; + case _VersionIncrementType.build: + throw UnimplementedError( + 'Build version changes should not happen in batch mode. Please file an issue if you see this.', + ); + case null: + type = VersionChange.skip; + } + + final String changelogEntry = getStringArg(_changelogFlag); + final content = + ''' +changelog: | +${changelogEntry.split('\n').map((line) => ' - $line').join('\n')} +version: ${type.name} +'''; + final now = DateTime.now(); + final date = + '${now.year}_${now.month.toString().padLeft(2, '0')}_${now.day.toString().padLeft(2, '0')}'; + final filename = 'change_${date}_${now.millisecondsSinceEpoch}.yaml'; + final File file = pendingDirectory.childFile(filename); + file.writeAsStringSync(content); + print('${indentation}Created pending changelog entry: $filename'); + return PackageResult.success(); + } } diff --git a/script/tool/test/update_release_info_command_test.dart b/script/tool/test/update_release_info_command_test.dart index 0ad6888008f..58d30362dfd 100644 --- a/script/tool/test/update_release_info_command_test.dart +++ b/script/tool/test/update_release_info_command_test.dart @@ -779,4 +779,235 @@ packages/a_package/test/plugin_test.dart ); }); }); + + group('batch release', () { + test('creates pending changelog for bugfix', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + version: '1.0.0', + ); + package.ciConfigFile.writeAsStringSync('release:\n batch: true'); + package.pendingChangelogsDirectory.createSync(); + const originalChangelog = ''' +## 1.0.0 + +* Previous changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.0'); + expect(package.changelogFile.readAsStringSync(), originalChangelog); + expect( + output, + containsAllInOrder([ + contains(' Created pending changelog entry: change_'), + ]), + ); + + final List pendingFiles = package.pendingChangelogsDirectory + .listSync() + .whereType() + .toList(); + expect(pendingFiles, hasLength(1)); + final String content = pendingFiles.first.readAsStringSync(); + expect(content, contains('changelog: |\n - A change.')); + expect(content, contains('version: patch')); + }); + + test('creates pending changelog for minor', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + version: '1.0.0', + ); + package.ciConfigFile.writeAsStringSync('release:\n batch: true'); + package.pendingChangelogsDirectory.createSync(); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ]); + + expect( + output, + containsAllInOrder([ + contains(' Created pending changelog entry: change_'), + ]), + ); + + final List pendingFiles = package.pendingChangelogsDirectory + .listSync() + .whereType() + .toList(); + expect(pendingFiles, hasLength(1)); + final String content = pendingFiles.first.readAsStringSync(); + expect(content, contains('version: minor')); + }); + + test('creates pending changelog for next (skip)', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + version: '1.0.0', + ); + package.ciConfigFile.writeAsStringSync('release:\n batch: true'); + package.pendingChangelogsDirectory.createSync(); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.', + ]); + + expect( + output, + containsAllInOrder([ + contains(' Created pending changelog entry: change_'), + ]), + ); + + final List pendingFiles = package.pendingChangelogsDirectory + .listSync() + .whereType() + .toList(); + expect(pendingFiles, hasLength(1)); + final String content = pendingFiles.first.readAsStringSync(); + expect(content, contains('version: skip')); + }); + + test( + 'creates pending changelog for minimal with publish-worthy changes', + () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + version: '1.0.0', + ); + package.ciConfigFile.writeAsStringSync('release:\n batch: true'); + package.pendingChangelogsDirectory.createSync(); + gitProcessRunner.mockProcessesForExecutable['git-diff'] = + [ + FakeProcessInfo( + MockProcess( + stdout: ''' +packages/a_package/lib/plugin.dart +''', + ), + ), + ]; + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minimal', + '--changelog', + 'A change.', + ]); + + expect( + output, + containsAllInOrder([ + contains( + RegExp( + r' Created pending changelog entry: change_\d{4}_\d{2}_\d{2}_\d+\.yaml', + ), + ), + ]), + ); + + final List pendingFiles = package.pendingChangelogsDirectory + .listSync() + .whereType() + .toList(); + expect(pendingFiles, hasLength(1)); + final String content = pendingFiles.first.readAsStringSync(); + expect(content, contains('version: patch')); + }, + ); + + test('skips for minimal with no changes (batch mode)', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + version: '1.0.0', + ); + package.ciConfigFile.writeAsStringSync('release:\n batch: true'); + gitProcessRunner.mockProcessesForExecutable['git-diff'] = + [ + FakeProcessInfo( + MockProcess( + stdout: ''' +packages/different_package/lib/foo.dart +''', + ), + ), + ]; + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minimal', + '--changelog', + 'A change.', + ]); + + expect( + output, + containsAllInOrder([ + contains('No changes to package'), + contains('Skipped 1 package'), + ]), + ); + // No pending changelog should be created. + expect(package.pendingChangelogsDirectory.existsSync(), isFalse); + }); + + test('fails for pre-release version', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + version: '1.0.0-dev.1', + ); + package.ciConfigFile.writeAsStringSync('release:\n batch: true'); + const originalChangelog = ''' +## 1.0.0-dev.1 + +* Previous changes. +'''; + package.changelogFile.writeAsStringSync(originalChangelog); + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output.join('\n'), + contains( + 'This command does not support batch releases packages with pre-release versions.\n' + ' Pre-release version: 1.0.0-dev.1', + ), + ); + }); + }); }