mirror of
https://github.com/flutter/packages.git
synced 2025-05-31 05:30:36 +08:00
[tools] Add update-release-info
(#5643)
This commit is contained in:
95
script/tool/lib/src/common/package_state_utils.dart
Normal file
95
script/tool/lib/src/common/package_state_utils.dart
Normal file
@ -0,0 +1,95 @@
|
||||
// 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<String> 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<String> components = p.posix.split(packageRelativePath);
|
||||
if (components.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final bool isChangelog = components.first == 'CHANGELOG.md';
|
||||
if (isChangelog) {
|
||||
hasChangelogChange = true;
|
||||
}
|
||||
|
||||
if (!needsVersionChange &&
|
||||
!isChangelog &&
|
||||
// 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' &&
|
||||
!<String>{'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') &&
|
||||
!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);
|
||||
}
|
@ -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<String> args) {
|
||||
..addCommand(ReadmeCheckCommand(packagesDir))
|
||||
..addCommand(TestCommand(packagesDir))
|
||||
..addCommand(UpdateExcerptsCommand(packagesDir))
|
||||
..addCommand(UpdateReleaseInfoCommand(packagesDir))
|
||||
..addCommand(VersionCheckCommand(packagesDir))
|
||||
..addCommand(XcodeAnalyzeCommand(packagesDir));
|
||||
|
||||
|
310
script/tool/lib/src/update_release_info_command.dart
Normal file
310
script/tool/lib/src/update_release_info_command.dart
Normal file
@ -0,0 +1,310 @@
|
||||
// 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: <String>[
|
||||
_versionNext,
|
||||
_versionMinimal,
|
||||
_versionBugfix,
|
||||
_versionMinor,
|
||||
],
|
||||
allowedHelp: <String, String>{
|
||||
_versionNext:
|
||||
'No version change; just adds a NEXT entry to the changelog.',
|
||||
_versionBugfix: 'Increments the bugfix version.',
|
||||
_versionMinor: 'Increments the minor version.',
|
||||
_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).',
|
||||
});
|
||||
}
|
||||
|
||||
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<String> _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<void> 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<PackageResult> runForPackage(RepositoryPackage package) async {
|
||||
String nextVersionString;
|
||||
|
||||
_VersionIncrementType? versionChange = _versionChange;
|
||||
|
||||
// If the change type is `minimal` determine what changes, if any, are
|
||||
// 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.hasChanges) {
|
||||
return PackageResult.skip('No changes to package');
|
||||
}
|
||||
if (state.needsVersionChange) {
|
||||
versionChange = _VersionIncrementType.bugfix;
|
||||
}
|
||||
}
|
||||
|
||||
if (versionChange != null) {
|
||||
final Version? updatedVersion =
|
||||
_updatePubspecVersion(package, versionChange);
|
||||
if (updatedVersion == null) {
|
||||
return PackageResult.fail(
|
||||
<String>['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(<String>['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.
|
||||
<String>[
|
||||
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.
|
||||
<String>[
|
||||
..._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<String> _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(<String>['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}');
|
||||
}
|
||||
}
|
||||
}
|
@ -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';
|
||||
@ -531,44 +532,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;
|
||||
getRelativePosixPath(package.directory, from: gitRoot);
|
||||
|
||||
final List<String> components = p.posix.split(path);
|
||||
final bool isChangelog = components.last == 'CHANGELOG.md';
|
||||
if (isChangelog) {
|
||||
hasChangelogChange = true;
|
||||
}
|
||||
final PackageChangeState state = checkPackageChangeState(package,
|
||||
changedPaths: _changedFiles, relativePackagePath: relativePackagePath);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 '
|
||||
@ -586,7 +559,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 '
|
||||
|
Reference in New Issue
Block a user