From cde14d8cd9c5295e930245b1c431694186451796 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 5 May 2022 11:20:54 -0400 Subject: [PATCH 1/5] [tools] Add `update-release-info` Because of our continuous release model, and our policy of adding CHANGELOG entries even for things that don't necessarily affect plugin clients, some bulk changes are very tedious to make because of the need to manually update many CHANGELOG.md and pubspec.yaml files. We also commonly have to leave review comments about CHANGELOG.md updates that don't follow our style guide (missing periods, incorrect list format, etc.) or don't correctly handle `## NEXT`. To address these issues, this adds a new command that can automatically update versions (for a given change level, handling pre-1.0 and post-1.0 version conventions) and CHANGELOGs for multiple packages at once. This will become the recommended way to make updates for non-major changes on the wiki page linked from the PR checklist. Does not support major versions, since: - we should never need to make bulk major version changes, and - the changelog entries for a major version are often complicated and thus not well suited to a command line flag, and usually require review cycles anyway. --- script/tool/CHANGELOG.md | 5 + script/tool/README.md | 26 + .../lib/src/common/package_state_utils.dart | 93 +++ script/tool/lib/src/main.dart | 2 + .../lib/src/update_release_info_command.dart | 303 +++++++++ .../tool/lib/src/version_check_command.dart | 41 +- script/tool/pubspec.yaml | 2 +- .../test/common/package_state_utils_test.dart | 92 +++ .../update_release_info_command_test.dart | 617 ++++++++++++++++++ script/tool/test/util.dart | 14 +- 10 files changed, 1156 insertions(+), 39 deletions(-) create mode 100644 script/tool/lib/src/common/package_state_utils.dart create mode 100644 script/tool/lib/src/update_release_info_command.dart create mode 100644 script/tool/test/common/package_state_utils_test.dart create mode 100644 script/tool/test/update_release_info_command_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index b2319c63dc48..e251c716c136 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.8.6 + +- Adds `update-release-info` to apply changelog and optional version changes + across multiple packages. + ## 0.8.5 - Updates `test` to inculde the Dart unit tests of examples, if any. diff --git a/script/tool/README.md b/script/tool/README.md index d52ee08fdc34..b2629f77b8c4 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -118,6 +118,32 @@ cd dart run ./script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages plugin_name ``` +### Update CHANGELOG and Version + +`update-release-info` will automatically update the version and `CHANGELOG.md` +following standard repository style and practice. It can be used for +single-package updates to handled the details of getting the `CHANGELOG.md` +format correct, but is especially useful for bulk updates. + +For instance, if you've added a new analysis option that required production +code changes across many packages: + +```sh +cd +dart run ./script/tool/bin/flutter_plugin_tools.dart update-release-info \ + --version=minimal \ + --changelog="Fixes violations of new analysis option some_new_option." + --run-on-changed-packages +``` + +The `minimal` option for `--version` will treat each package as either `bugfix` +or `next` depending on the files that have changed in that package, so is +often the best choice for a bulk change. + +For cases where you know the change time, `minor` or `bugfix` will make the +corresponding version bump, or `next` will update only `CHANGELOG.md` without +changing the version. + ### Publish a Release **Releases are automated for `flutter/plugins` and `flutter/packages`.** diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart new file mode 100644 index 000000000000..af123cf3e067 --- /dev/null +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -0,0 +1,93 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +import 'repository_package.dart'; + +/// The state of a package on disk relative to git state. +@immutable +class PackageChangeState { + /// Creates a new immutable state instance. + const PackageChangeState({ + required this.hasChanges, + required this.hasChangelogChange, + required this.needsVersionChange, + }); + + /// True if there are any changes to files in the package. + final bool hasChanges; + + /// True if the package's CHANGELOG.md has been changed. + final bool hasChangelogChange; + + /// True if any changes in the package require a version change according + /// to repository policy. + final bool needsVersionChange; +} + +/// Checks [package] against [changedPaths] to determine what changes it has +/// and how those changes relate to repository policy about CHANGELOG and +/// version updates. +/// +/// [changedPaths] should be a list of POSIX-style paths from a common root, +/// and [relativePackagePath] should be the path to [package] from that same +/// root. Commonly these will come from `gitVersionFinder.getChangedFiles()` +/// and `getRelativePoixPath(package.directory, gitDir.path)` respectively; +/// they are arguments mainly to allow for caching the changed paths for an +/// entire command run. +PackageChangeState checkPackageChangeState( + RepositoryPackage package, { + required List changedPaths, + required String relativePackagePath, +}) { + final String packagePrefix = relativePackagePath.endsWith('/') + ? relativePackagePath + : '$relativePackagePath/'; + + bool hasChanges = false; + bool hasChangelogChange = false; + bool needsVersionChange = false; + for (final String path in changedPaths) { + // Only consider files within the package. + if (!path.startsWith(packagePrefix)) { + continue; + } + final String packageRelativePath = path.substring(packagePrefix.length); + hasChanges = true; + + final List components = p.posix.split(packageRelativePath); + if (components.isEmpty) { + continue; + } + final bool isChangelog = components.first == 'CHANGELOG.md'; + if (isChangelog) { + hasChangelogChange = true; + } + + if (!needsVersionChange && + !isChangelog && + // The example's main.dart is shown on pub.dev, but for anything else + // in the example publishing has no purpose. + !(components.contains('example') && components.last != 'main.dart') && + // Changes to tests don't need to be published. + !components.contains('test') && + !components.contains('androidTest') && + !components.contains('RunnerTests') && + !components.contains('RunnerUITests') && + // The top-level "tool" directory is for non-client-facing utility code, + // so doesn't need to be published. + components.first != 'tool' && + // Ignoring lints doesn't affect clients. + !components.contains('lint-baseline.xml')) { + needsVersionChange = true; + } + } + + return PackageChangeState( + hasChanges: hasChanges, + hasChangelogChange: hasChangelogChange, + needsVersionChange: needsVersionChange); +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 9c572ee270e0..739aef56878d 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -29,6 +29,7 @@ import 'pubspec_check_command.dart'; import 'readme_check_command.dart'; import 'test_command.dart'; import 'update_excerpts_command.dart'; +import 'update_release_info_command.dart'; import 'version_check_command.dart'; import 'xcode_analyze_command.dart'; @@ -70,6 +71,7 @@ void main(List args) { ..addCommand(ReadmeCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) ..addCommand(UpdateExcerptsCommand(packagesDir)) + ..addCommand(UpdateReleaseInfoCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) ..addCommand(XcodeAnalyzeCommand(packagesDir)); diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart new file mode 100644 index 000000000000..ab2e8cc60e34 --- /dev/null +++ b/script/tool/lib/src/update_release_info_command.dart @@ -0,0 +1,303 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +import 'common/git_version_finder.dart'; +import 'common/package_looping_command.dart'; +import 'common/package_state_utils.dart'; +import 'common/repository_package.dart'; + +/// Supported version change types, from smallest to largest component. +enum _VersionIncrementType { build, bugfix, minor } + +/// Possible results of attempting to update a CHANGELOG.md file. +enum _ChangelogUpdateOutcome { addedSection, updatedSection, failed } + +/// A state machine for the process of updating a CHANGELOG.md. +enum _ChangelogUpdateState { + /// Looking for the first version section. + findingFirstSection, + + /// Looking for the first list entry in an existing section. + findingFirstListItem, + + /// Finished with updates. + finishedUpdating, +} + +/// A command to update the changelog, and optionally version, of packages. +class UpdateReleaseInfoCommand extends PackageLoopingCommand { + /// Creates a publish metadata updater command instance. + UpdateReleaseInfoCommand( + Directory packagesDir, { + GitDir? gitDir, + }) : super(packagesDir, gitDir: gitDir) { + argParser.addOption(_changelogFlag, + mandatory: true, + help: 'The changelog entry to add. ' + 'Each line will be a separate list entry.'); + argParser.addOption(_versionTypeFlag, + mandatory: true, + help: 'The version change level', + allowed: [ + _versionNext, + _versionMinimal, + _versionBugfix, + _versionMinor, + ], + allowedHelp: { + _versionNext: + 'No version change; just adds a NEXT entry to the changelog.', + _versionBugfix: 'Increments the bugfix version.', + _versionMinor: 'Increments the minor version.', + _versionMinimal: 'Either increments the bugfix version or uses NEXT ' + 'depending on the files changed.', + }); + } + + static const String _changelogFlag = 'changelog'; + static const String _versionTypeFlag = 'version'; + + static const String _versionNext = 'next'; + static const String _versionBugfix = 'bugfix'; + static const String _versionMinor = 'minor'; + static const String _versionMinimal = 'minimal'; + + // The version change type, if there is a set type for all platforms. + // + // If null, either there is no version change, or it is dynamic (`minimal`). + _VersionIncrementType? _versionChange; + + // The cache of changed files, for dynamic version change determination. + // + // Only set for `minimal` version change. + late final List _changedFiles; + + @override + final String name = 'update-release-info'; + + @override + final String description = 'Updates CHANGELOG.md files, and optionally the ' + 'version in pubspec.yaml, in a way that is consistent with version-check ' + 'enforcement.'; + + @override + bool get hasLongOutput => false; + + @override + Future initializeRun() async { + if (getStringArg(_changelogFlag).trim().isEmpty) { + throw UsageException('Changelog message must not be empty.', usage); + } + switch (getStringArg(_versionTypeFlag)) { + case _versionMinor: + _versionChange = _VersionIncrementType.minor; + break; + case _versionBugfix: + _versionChange = _VersionIncrementType.bugfix; + break; + case _versionMinimal: + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + _changedFiles = await gitVersionFinder.getChangedFiles(); + // Anothing other than a fixed change is null. + _versionChange = null; + break; + case _versionNext: + _versionChange = null; + break; + default: + throw UnimplementedError('Unimplemented version change type'); + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + String nextVersionString; + + _VersionIncrementType? versionChange = _versionChange; + // If the change type is `minimal`, determine whether a version change is + // needed. + if (versionChange == null && + getStringArg(_versionTypeFlag) == _versionMinimal) { + final Directory gitRoot = + packagesDir.fileSystem.directory((await gitDir).path); + final String relativePackagePath = + getRelativePosixPath(package.directory, from: gitRoot); + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: _changedFiles, + relativePackagePath: relativePackagePath); + if (state.needsVersionChange) { + versionChange = _VersionIncrementType.bugfix; + } + } + + 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.'); + break; + case _ChangelogUpdateOutcome.updatedSection: + print('${indentation}Updated NEXT section.'); + break; + case _ChangelogUpdateOutcome.failed: + return PackageResult.fail(['Could not update CHANGELOG.md.']); + } + + return PackageResult.success(); + } + + _ChangelogUpdateOutcome _updateChangelog( + RepositoryPackage package, String version) { + if (!package.changelogFile.existsSync()) { + printError('${indentation}Missing CHANGELOG.md.'); + return _ChangelogUpdateOutcome.failed; + } + + final String newHeader = '## $version'; + final RegExp listItemPattern = RegExp(r'^(\s*[-*])'); + + final StringBuffer newChangelog = StringBuffer(); + _ChangelogUpdateState state = _ChangelogUpdateState.findingFirstSection; + bool updatedExistingSection = false; + + for (final String line in package.changelogFile.readAsLinesSync()) { + switch (state) { + case _ChangelogUpdateState.findingFirstSection: + final String trimmedLine = line.trim(); + if (trimmedLine.isEmpty) { + // Discard any whitespace at the top of the file. + } else if (trimmedLine == '## NEXT') { + // Replace the header with the new version (which may also be NEXT). + newChangelog.writeln(newHeader); + // Find the existing list to add to. + state = _ChangelogUpdateState.findingFirstListItem; + } else { + // The first content in the file isn't a NEXT section, so just add + // the new section. + [ + newHeader, + '', + ..._changelogAdditionsAsList(), + '', + line, // Don't drop the current line. + ].forEach(newChangelog.writeln); + state = _ChangelogUpdateState.finishedUpdating; + } + break; + case _ChangelogUpdateState.findingFirstListItem: + final RegExpMatch? match = listItemPattern.firstMatch(line); + if (match != null) { + final String listMarker = match[1]!; + // Add the new items on top. If the new change is changing the + // version, then the new item should be more relevant to package + // clients than anything that was already there. If it's still + // NEXT, the order doesn't matter. + [ + ..._changelogAdditionsAsList(listMarker: listMarker), + line, // Don't drop the current line. + ].forEach(newChangelog.writeln); + state = _ChangelogUpdateState.finishedUpdating; + updatedExistingSection = true; + } else if (line.trim().isEmpty) { + // Scan past empty lines, but keep them. + newChangelog.writeln(line); + } else { + printError(' Existing NEXT section has unrecognized format.'); + return _ChangelogUpdateOutcome.failed; + } + break; + case _ChangelogUpdateState.finishedUpdating: + // Once changes are done, add the rest of the lines as-is. + newChangelog.writeln(line); + break; + } + } + + package.changelogFile.writeAsStringSync(newChangelog.toString()); + + return updatedExistingSection + ? _ChangelogUpdateOutcome.updatedSection + : _ChangelogUpdateOutcome.addedSection; + } + + /// Returns the changelog to add as a Markdown list, using the given list + /// bullet style (default to the repository standard of '*'), and adding + /// any missing periods. + /// + /// E.g., 'A line\nAnother line.' will become: + /// ``` + /// [ '* A line.', '* Another line.' ] + /// ``` + Iterable _changelogAdditionsAsList({String listMarker = '*'}) { + return getStringArg(_changelogFlag).split('\n').map((String entry) { + String standardizedEntry = entry.trim(); + if (!standardizedEntry.endsWith('.')) { + standardizedEntry = '$standardizedEntry.'; + } + return '$listMarker $standardizedEntry'; + }); + } + + /// Updates the version in [package]'s pubspec according to [type], returning + /// the new version, or null if there was an error updating the version. + Version? _updatePubspecVersion( + RepositoryPackage package, _VersionIncrementType type) { + final Pubspec pubspec = package.parsePubspec(); + final Version? currentVersion = pubspec.version; + if (currentVersion == null) { + printError('${indentation}No version in pubspec.yaml'); + return null; + } + + // For versions less than 1.0, shift the change down one component per + // Dart versioning conventions. + final _VersionIncrementType adjustedType = currentVersion.major > 0 + ? type + : _VersionIncrementType.values[type.index - 1]; + + final Version newVersion = _nextVersion(currentVersion, adjustedType); + + // Write the new version to the pubspec. + final YamlEditor editablePubspec = + YamlEditor(package.pubspecFile.readAsStringSync()); + editablePubspec.update(['version'], newVersion.toString()); + package.pubspecFile.writeAsStringSync(editablePubspec.toString()); + + return newVersion; + } + + Version _nextVersion(Version version, _VersionIncrementType type) { + switch (type) { + case _VersionIncrementType.minor: + return version.nextMinor; + case _VersionIncrementType.bugfix: + return version.nextPatch; + case _VersionIncrementType.build: + final int buildNumber = + version.build.isEmpty ? 0 : version.build.first as int; + return Version(version.major, version.minor, version.patch, + build: '${buildNumber + 1}'); + } + } +} diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index fb768c19b524..eba89ffbe2f7 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -13,6 +13,7 @@ import 'package:pub_semver/pub_semver.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; import 'common/package_looping_command.dart'; +import 'common/package_state_utils.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; import 'common/repository_package.dart'; @@ -526,44 +527,16 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. final Directory gitRoot = packagesDir.fileSystem.directory((await gitDir).path); final String relativePackagePath = - getRelativePosixPath(package.directory, from: gitRoot) + '/'; - bool hasChanges = false; - bool needsVersionChange = false; - bool hasChangelogChange = false; - for (final String path in _changedFiles) { - // Only consider files within the package. - if (!path.startsWith(relativePackagePath)) { - continue; - } - hasChanges = true; - - final List components = p.posix.split(path); - final bool isChangelog = components.last == 'CHANGELOG.md'; - if (isChangelog) { - hasChangelogChange = true; - } + getRelativePosixPath(package.directory, from: gitRoot); - if (!needsVersionChange && - !isChangelog && - // The example's main.dart is shown on pub.dev, but for anything else - // in the example publishing has no purpose. - !(components.contains('example') && components.last != 'main.dart') && - // Changes to tests don't need to be published. - !components.contains('test') && - !components.contains('androidTest') && - !components.contains('RunnerTests') && - !components.contains('RunnerUITests') && - // Ignoring lints doesn't affect clients. - !components.contains('lint-baseline.xml')) { - needsVersionChange = true; - } - } + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: _changedFiles, relativePackagePath: relativePackagePath); - if (!hasChanges) { + if (!state.hasChanges) { return null; } - if (needsVersionChange) { + if (state.needsVersionChange) { if (_getChangeDescription().split('\n').any((String line) => line.startsWith(_missingVersionChangeJustificationMarker))) { logWarning('Ignoring lack of version change due to ' @@ -581,7 +554,7 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. } } - if (!hasChangelogChange) { + if (!state.hasChangelogChange) { if (_getChangeDescription().split('\n').any((String line) => line.startsWith(_missingChangelogChangeJustificationMarker))) { logWarning('Ignoring lack of CHANGELOG update due to ' diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 32bfc1b62281..138c1183fa1c 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.8.5 +version: 0.8.6 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart new file mode 100644 index 000000000000..d3c6e78b43dc --- /dev/null +++ b/script/tool/test/common/package_state_utils_test.dart @@ -0,0 +1,92 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/package_state_utils.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + }); + + group('pluginSupportsPlatform', () { + test('reports version change needed for code changes', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + const List changedFiles = [ + 'packages/a_package/lib/plugin.dart', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_package'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + + test('handles trailing slash on package path', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + const List changedFiles = [ + 'packages/a_package/lib/plugin.dart', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_package/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + expect(state.hasChangelogChange, false); + }); + + test('does not report version change exempt changes', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/android/lint-baseline.xml', + 'packages/a_plugin/example/android/src/androidTest/foo/bar/FooTest.java', + 'packages/a_plugin/example/ios/RunnerTests/Foo.m', + 'packages/a_plugin/example/ios/RunnerUITests/info.plist', + 'packages/a_plugin/tool/a_development_tool.dart', + 'packages/a_plugin/CHANGELOG.md', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, false); + expect(state.hasChangelogChange, true); + }); + + test('only considers a root "tool" folder to be special', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/lib/foo/tool/tool_thing.dart', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + }); +} diff --git a/script/tool/test/update_release_info_command_test.dart b/script/tool/test/update_release_info_command_test.dart new file mode 100644 index 000000000000..2b60a366d8a2 --- /dev/null +++ b/script/tool/test/update_release_info_command_test.dart @@ -0,0 +1,617 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/update_release_info_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + late MockGitDir gitDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + + gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through a process runner, to make mock output + // consistent with other processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); + }); + + final UpdateReleaseInfoCommand command = UpdateReleaseInfoCommand( + packagesDir, + gitDir: gitDir, + ); + runner = CommandRunner( + 'update_release_info_command', 'Test for update_release_info_command'); + runner.addCommand(command); + }); + + group('flags', () { + test('fails if --changelog is missing', () async { + Exception? commandError; + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + ], exceptionHandler: (Exception e) { + commandError = e; + }); + + expect(commandError, isA()); + }); + + test('fails if --changelog is blank', () async { + Exception? commandError; + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + '', + ], exceptionHandler: (Exception e) { + commandError = e; + }); + + expect(commandError, isA()); + }); + + test('fails if --version is missing', () async { + Exception? commandError; + await runCapturingPrint( + runner, ['update-release-info', '--changelog', ''], + exceptionHandler: (Exception e) { + commandError = e; + }); + + expect(commandError, isA()); + }); + + test('fails if --version is an unknown value', () async { + Exception? commandError; + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=foo', + '--changelog', + '', + ], exceptionHandler: (Exception e) { + commandError = e; + }); + + expect(commandError, isA()); + }); + }); + + group('changelog', () { + test('adds new NEXT section', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## 1.0.0 + +* Previous changes. +'''; + createFakeCHANGELOG(package, originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## NEXT + +* A change. + +$originalChangelog'''; + + expect( + output, + containsAllInOrder([ + contains(' Added a NEXT section.'), + ]), + ); + expect(newChangelog, expectedChangeLog); + }); + + test('adds to existing NEXT section', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## NEXT + +* Already-pending changes. + +## 1.0.0 + +* Old changes. +'''; + createFakeCHANGELOG(package, originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## NEXT + +* A change. +* Already-pending changes. + +## 1.0.0 + +* Old changes. +'''; + + expect(output, + containsAllInOrder([contains(' Updated NEXT section.')])); + expect(newChangelog, expectedChangeLog); + }); + + test('adds new version section', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## 1.0.0 + +* Previous changes. +'''; + createFakeCHANGELOG(package, originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## 1.0.1 + +* A change. + +$originalChangelog'''; + + expect( + output, + containsAllInOrder([ + contains(' Added a 1.0.1 section.'), + ]), + ); + expect(newChangelog, expectedChangeLog); + }); + + test('converts existing NEXT section to version section', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## NEXT + +* Already-pending changes. + +## 1.0.0 + +* Old changes. +'''; + createFakeCHANGELOG(package, originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## 1.0.1 + +* A change. +* Already-pending changes. + +## 1.0.0 + +* Old changes. +'''; + + expect(output, + containsAllInOrder([contains(' Updated NEXT section.')])); + expect(newChangelog, expectedChangeLog); + }); + + test('treats multiple lines as multiple list items', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## 1.0.0 + +* Previous changes. +'''; + createFakeCHANGELOG(package, originalChangelog); + + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'First change.\nSecond change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## 1.0.1 + +* First change. +* Second change. + +$originalChangelog'''; + + expect(newChangelog, expectedChangeLog); + }); + + test('adds a period to any lines missing it, and removes whitespace', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## 1.0.0 + +* Previous changes. +'''; + createFakeCHANGELOG(package, originalChangelog); + + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'First change \nSecond change' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## 1.0.1 + +* First change. +* Second change. + +$originalChangelog'''; + + expect(newChangelog, expectedChangeLog); + }); + + test('handles non-standard changelog format', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +# 1.0.0 + +* A version with the wrong heading format. +'''; + createFakeCHANGELOG(package, originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## NEXT + +* A change. + +$originalChangelog'''; + + expect(output, + containsAllInOrder([contains(' Added a NEXT section.')])); + expect(newChangelog, expectedChangeLog); + }); + + test('adds to existing NEXT section using - list style', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## NEXT + + - Already-pending changes. + +## 1.0.0 + + - Previous changes. +'''; + createFakeCHANGELOG(package, originalChangelog); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String newChangelog = package.changelogFile.readAsStringSync(); + const String expectedChangeLog = ''' +## NEXT + + - A change. + - Already-pending changes. + +## 1.0.0 + + - Previous changes. +'''; + + expect(output, + containsAllInOrder([contains(' Updated NEXT section.')])); + expect(newChangelog, expectedChangeLog); + }); + + test('fails if CHANGELOG.md is missing', () async { + createFakePackage('a_package', packagesDir, includeCommonFiles: false); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output, + containsAllInOrder([contains(' Missing CHANGELOG.md.')])); + }); + + test('fails if CHANGELOG.md has unexpected NEXT block format', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + const String originalChangelog = ''' +## NEXT + +Some free-form text that isn't a list. + +## 1.0.0 + +- Previous changes. +'''; + createFakeCHANGELOG(package, originalChangelog); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains(' Existing NEXT section has unrecognized format.') + ])); + }); + }); + + group('pubspec', () { + test('does not change for --next', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.0'); + + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=next', + '--changelog', + 'A change.' + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.0'); + }); + + test('updates bugfix version for pre-1.0 without existing build number', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '0.1.0'); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '0.1.0+1'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 0.1.0+1')])); + }); + + test('updates bugfix version for pre-1.0 with existing build number', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '0.1.0+2'); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=bugfix', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '0.1.0+3'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 0.1.0+3')])); + }); + + test('updates bugfix version for post-1.0', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + + 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.2'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 1.0.2')])); + }); + + test('updates minor version for pre-1.0', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '0.1.0+2'); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '0.1.1'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 0.1.1')])); + }); + + test('updates minor version for post-1.0', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.1.0'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 1.1.0')])); + }); + + test('updates bugfix version for "minimal" with publish-worthy changes', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/a_package/lib/plugin.dart +'''), + ]; + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minimal', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.2'); + expect( + output, + containsAllInOrder( + [contains(' Incremented version to 1.0.2')])); + }); + + test('no version change for "minimal" with non-publish-worthy changes', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/a_package/test/plugin_test.dart +'''), + ]; + + await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minimal', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.1'); + }); + + test('fails if there is no version in pubspec', () async { + createFakePackage('a_package', packagesDir, version: null); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minor', + '--changelog', + 'A change.', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [contains('Could not determine current version.')])); + }); + }); +} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 5c38bd5f7033..7f0b83be2769 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -319,14 +319,15 @@ String _pluginPlatformSection( return entry; } -typedef _ErrorHandler = void Function(Error error); - /// Run the command [runner] with the given [args] and return /// what was printed. /// A custom [errorHandler] can be used to handle the runner error as desired without throwing. Future> runCapturingPrint( - CommandRunner runner, List args, - {_ErrorHandler? errorHandler}) async { + CommandRunner runner, + List args, { + Function(Error error)? errorHandler, + Function(Exception error)? exceptionHandler, +}) async { final List prints = []; final ZoneSpecification spec = ZoneSpecification( print: (_, __, ___, String message) { @@ -342,6 +343,11 @@ Future> runCapturingPrint( rethrow; } errorHandler(e); + } on Exception catch (e) { + if (exceptionHandler == null) { + rethrow; + } + exceptionHandler(e); } return prints; From 1e6e3e39f4dab126d587e26a3030e771a8243d27 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 5 May 2022 17:09:43 -0700 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com> --- script/tool/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/script/tool/README.md b/script/tool/README.md index b2629f77b8c4..35190ed35597 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -122,10 +122,10 @@ dart run ./script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages `update-release-info` will automatically update the version and `CHANGELOG.md` following standard repository style and practice. It can be used for -single-package updates to handled the details of getting the `CHANGELOG.md` -format correct, but is especially useful for bulk updates. +single-package updates to handle the details of getting the `CHANGELOG.md` +format correct, but is especially useful for bulk updates across multiple packages. -For instance, if you've added a new analysis option that required production +For instance, if you add a new analysis option that requires production code changes across many packages: ```sh @@ -137,7 +137,7 @@ dart run ./script/tool/bin/flutter_plugin_tools.dart update-release-info \ ``` The `minimal` option for `--version` will treat each package as either `bugfix` -or `next` depending on the files that have changed in that package, so is +or `next` depending on the files that have changed in that package, so it is often the best choice for a bulk change. For cases where you know the change time, `minor` or `bugfix` will make the From 0d42f3f066d476f10102c2fe47421f1e8b16d9fb Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 5 May 2022 20:21:29 -0400 Subject: [PATCH 3/5] Add md file handling --- .../lib/src/common/package_state_utils.dart | 8 +-- .../test/common/package_state_utils_test.dart | 50 ++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index af123cf3e067..437bbf6df370 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -69,9 +69,11 @@ PackageChangeState checkPackageChangeState( if (!needsVersionChange && !isChangelog && - // The example's main.dart is shown on pub.dev, but for anything else - // in the example publishing has no purpose. - !(components.contains('example') && components.last != 'main.dart') && + // One of a few special files example will be shown on pub.dev, but for + // anything else in the example publishing has no purpose. + !(components.first == 'example' && + !{'main.dart', 'readme.md', 'example.md'} + .contains(components.last.toLowerCase())) && // Changes to tests don't need to be published. !components.contains('test') && !components.contains('androidTest') && diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart index d3c6e78b43dc..cc9116a9ea25 100644 --- a/script/tool/test/common/package_state_utils_test.dart +++ b/script/tool/test/common/package_state_utils_test.dart @@ -17,7 +17,7 @@ void main() { packagesDir = createPackagesDirectory(fileSystem: fileSystem); }); - group('pluginSupportsPlatform', () { + group('checkPackageChangeState', () { test('reports version change needed for code changes', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir); @@ -88,5 +88,53 @@ void main() { expect(state.hasChanges, true); expect(state.needsVersionChange, true); }); + + test('requires a version change for example main', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/lib/main.dart', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + + test('requires a version change for example readme.md', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/README.md', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + + test('requires a version change for example example.md', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/lib/example.md', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); }); } From c82011255dd54bf3f53b5eeec55231e8919a23d9 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 6 May 2022 10:51:03 -0400 Subject: [PATCH 4/5] Skip unchanged packages for 'minimal' --- script/tool/README.md | 7 ++--- .../lib/src/update_release_info_command.dart | 13 +++++++-- .../update_release_info_command_test.dart | 28 +++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/script/tool/README.md b/script/tool/README.md index 35190ed35597..cc9621f44086 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -133,12 +133,11 @@ cd dart run ./script/tool/bin/flutter_plugin_tools.dart update-release-info \ --version=minimal \ --changelog="Fixes violations of new analysis option some_new_option." - --run-on-changed-packages ``` -The `minimal` option for `--version` will treat each package as either `bugfix` -or `next` depending on the files that have changed in that package, so it is -often the best choice for a bulk change. +The `minimal` option for `--version` will skip unchanged packages, and treat +each changed package as either `bugfix` or `next` depending on the files that +have changed in that package, so it is often the best choice for a bulk change. For cases where you know the change time, `minor` or `bugfix` will make the corresponding version bump, or `next` will update only `CHANGELOG.md` without diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart index ab2e8cc60e34..b998615ead17 100644 --- a/script/tool/lib/src/update_release_info_command.dart +++ b/script/tool/lib/src/update_release_info_command.dart @@ -57,8 +57,10 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand { 'No version change; just adds a NEXT entry to the changelog.', _versionBugfix: 'Increments the bugfix version.', _versionMinor: 'Increments the minor version.', - _versionMinimal: 'Either increments the bugfix version or uses NEXT ' - 'depending on the files changed.', + _versionMinimal: 'Depending on the changes to each package: ' + 'increments the bugfix version (for publishable changes), ' + "uses NEXT (for changes that don't need to be published), " + 'or skips (if no changes).', }); } @@ -122,7 +124,8 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand { String nextVersionString; _VersionIncrementType? versionChange = _versionChange; - // If the change type is `minimal`, determine whether a version change is + + // If the change type is `minimal` determine what changes, if any, are // needed. if (versionChange == null && getStringArg(_versionTypeFlag) == _versionMinimal) { @@ -133,6 +136,10 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand { final PackageChangeState state = checkPackageChangeState(package, changedPaths: _changedFiles, relativePackagePath: relativePackagePath); + + if (!state.hasChanges) { + return PackageResult.skip('No changes to package'); + } if (state.needsVersionChange) { versionChange = _VersionIncrementType.bugfix; } diff --git a/script/tool/test/update_release_info_command_test.dart b/script/tool/test/update_release_info_command_test.dart index 2b60a366d8a2..739018227f7a 100644 --- a/script/tool/test/update_release_info_command_test.dart +++ b/script/tool/test/update_release_info_command_test.dart @@ -383,6 +383,34 @@ $originalChangelog'''; expect(newChangelog, expectedChangeLog); }); + test('skips for "minimal" when there are no changes at all', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, version: '1.0.1'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/different_package/test/plugin_test.dart +'''), + ]; + final String originalChangelog = package.changelogFile.readAsStringSync(); + + final List output = await runCapturingPrint(runner, [ + 'update-release-info', + '--version=minimal', + '--changelog', + 'A change.', + ]); + + final String version = package.parsePubspec().version?.toString() ?? ''; + expect(version, '1.0.1'); + expect(package.changelogFile.readAsStringSync(), originalChangelog); + expect( + output, + containsAllInOrder([ + contains('No changes to package'), + contains('Skipped 1 package') + ])); + }); + test('fails if CHANGELOG.md is missing', () async { createFakePackage('a_package', packagesDir, includeCommonFiles: false); From f7d69f3f66e2989a5c8281cb2000e27b89e2df76 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 18 May 2022 12:26:56 -0400 Subject: [PATCH 5/5] Test updates from merge --- .../test/update_release_info_command_test.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/script/tool/test/update_release_info_command_test.dart b/script/tool/test/update_release_info_command_test.dart index 739018227f7a..7e7ff54d5947 100644 --- a/script/tool/test/update_release_info_command_test.dart +++ b/script/tool/test/update_release_info_command_test.dart @@ -113,7 +113,7 @@ void main() { * Previous changes. '''; - createFakeCHANGELOG(package, originalChangelog); + package.changelogFile.writeAsStringSync(originalChangelog); final List output = await runCapturingPrint(runner, [ 'update-release-info', @@ -152,7 +152,7 @@ $originalChangelog'''; * Old changes. '''; - createFakeCHANGELOG(package, originalChangelog); + package.changelogFile.writeAsStringSync(originalChangelog); final List output = await runCapturingPrint(runner, [ 'update-release-info', @@ -187,7 +187,7 @@ $originalChangelog'''; * Previous changes. '''; - createFakeCHANGELOG(package, originalChangelog); + package.changelogFile.writeAsStringSync(originalChangelog); final List output = await runCapturingPrint(runner, [ 'update-release-info', @@ -226,7 +226,7 @@ $originalChangelog'''; * Old changes. '''; - createFakeCHANGELOG(package, originalChangelog); + package.changelogFile.writeAsStringSync(originalChangelog); final List output = await runCapturingPrint(runner, [ 'update-release-info', @@ -261,7 +261,7 @@ $originalChangelog'''; * Previous changes. '''; - createFakeCHANGELOG(package, originalChangelog); + package.changelogFile.writeAsStringSync(originalChangelog); await runCapturingPrint(runner, [ 'update-release-info', @@ -292,7 +292,7 @@ $originalChangelog'''; * Previous changes. '''; - createFakeCHANGELOG(package, originalChangelog); + package.changelogFile.writeAsStringSync(originalChangelog); await runCapturingPrint(runner, [ 'update-release-info', @@ -322,7 +322,7 @@ $originalChangelog'''; * A version with the wrong heading format. '''; - createFakeCHANGELOG(package, originalChangelog); + package.changelogFile.writeAsStringSync(originalChangelog); final List output = await runCapturingPrint(runner, [ 'update-release-info', @@ -357,7 +357,7 @@ $originalChangelog'''; - Previous changes. '''; - createFakeCHANGELOG(package, originalChangelog); + package.changelogFile.writeAsStringSync(originalChangelog); final List output = await runCapturingPrint(runner, [ 'update-release-info', @@ -442,7 +442,7 @@ Some free-form text that isn't a list. - Previous changes. '''; - createFakeCHANGELOG(package, originalChangelog); + package.changelogFile.writeAsStringSync(originalChangelog); Error? commandError; final List output = await runCapturingPrint(runner, [