Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit a10b89e

Browse files
[flutter_plugin_tools] Check for missing version and CHANGELOG updates (#4530)
The currently documented repository policy is to: - require version updates for packages changes that don't meet specific exemptions, and - require CHANGELOG changes for essentially all changes. This adds tooling that enforces that policy, with a mechanism for overriding it via PR descriptions, to avoid cases where they are accidentally omitted without reviewers catching it. In order to facilitate testing (which require mocking another `git` command), this also updates the existing `version-check` tests: - Replaces the custom git result injection/validating with the newer bind-to-process-mocks approach that is now used in the rest of the tool tests. - Fixes some tests that were only checking for `ToolExit` to also check the error output, in order to ensure that failure tests are not accidentally passing for the wrong reason (as is being done in general as tests in the tooling are updated). Fixes flutter/flutter#93790
1 parent bb65f91 commit a10b89e

6 files changed

Lines changed: 686 additions & 169 deletions

File tree

.cirrus.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,16 @@ task:
7575
CHANGE_DESC: "$TMPDIR/change-description.txt"
7676
version_check_script:
7777
# For pre-submit, pass the PR description to the script to allow for
78-
# platform version breaking version change justifications.
79-
# For post-submit, ignore platform version breaking version changes.
80-
# The PR description isn't reliably part of the commit message, so using
81-
# the same flags as for presubmit would likely result in false-positive
82-
# post-submit failures.
78+
# version check overrides.
79+
# For post-submit, ignore platform version breaking version changes and
80+
# missing version/CHANGELOG detection; the PR description isn't reliably
81+
# part of the commit message, so using the same flags as for presubmit
82+
# would likely result in false-positive post-submit failures.
8383
- if [[ $CIRRUS_PR == "" ]]; then
8484
- ./script/tool_runner.sh version-check --ignore-platform-interface-breaks
8585
- else
8686
- echo "$CIRRUS_CHANGE_MESSAGE" > "$CHANGE_DESC"
87-
- ./script/tool_runner.sh version-check --change-description-file="$CHANGE_DESC"
87+
- ./script/tool_runner.sh version-check --check-for-missing-changes --change-description-file="$CHANGE_DESC"
8888
- fi
8989
publish_check_script: ./script/tool_runner.sh publish-check
9090
- name: format

script/tool/CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
## NEXT
1+
## 0.7.3
22

33
- `native-test` now builds unit tests before running them on Windows and Linux,
44
matching the behavior of other platforms.
5-
- Added `--log-timing` to add timing information to package headers in looping
5+
- Adds `--log-timing` to add timing information to package headers in looping
66
commands.
7+
- Adds a `--check-for-missing-changes` flag to `version-check` that requires
8+
version updates (except for recognized exemptions) and CHANGELOG changes when
9+
modifying packages, unless the PR description explains why it's not needed.
710

811
## 0.7.2
912

script/tool/lib/src/common/git_version_finder.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ class GitVersionFinder {
7575
io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand(
7676
<String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
7777
throwOnError: false);
78-
if (baseShaFromMergeBase.stderr != null ||
79-
baseShaFromMergeBase.stdout == null) {
78+
final String stdout = (baseShaFromMergeBase.stdout as String? ?? '').trim();
79+
final String stderr = (baseShaFromMergeBase.stdout as String? ?? '').trim();
80+
if (stderr.isNotEmpty || stdout.isEmpty) {
8081
baseShaFromMergeBase = await baseGitDir
8182
.runCommand(<String>['merge-base', 'FETCH_HEAD', 'HEAD']);
8283
}

script/tool/lib/src/version_check_command.dart

Lines changed: 139 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ class VersionCheckCommand extends PackageLoopingCommand {
120120
'(e.g., PR description or commit message).\n\n'
121121
'If supplied, this is used to allow overrides to some version '
122122
'checks.');
123+
argParser.addFlag(_checkForMissingChanges,
124+
help: 'Validates that changes to packages include CHANGELOG and '
125+
'version changes unless they meet an established exemption.\n\n'
126+
'If used with --$_changeDescriptionFile, this is should only be '
127+
'used in pre-submit CI checks, to prevent the possibility of '
128+
'post-submit breakage if an override justification is not '
129+
'transferred into the commit message.',
130+
hide: true);
123131
argParser.addFlag(_ignorePlatformInterfaceBreaks,
124132
help: 'Bypasses the check that platform interfaces do not contain '
125133
'breaking changes.\n\n'
@@ -133,6 +141,7 @@ class VersionCheckCommand extends PackageLoopingCommand {
133141

134142
static const String _againstPubFlag = 'against-pub';
135143
static const String _changeDescriptionFile = 'change-description-file';
144+
static const String _checkForMissingChanges = 'check-for-missing-changes';
136145
static const String _ignorePlatformInterfaceBreaks =
137146
'ignore-platform-interface-breaks';
138147

@@ -141,8 +150,26 @@ class VersionCheckCommand extends PackageLoopingCommand {
141150
static const String _breakingChangeJustificationMarker =
142151
'## Breaking change justification';
143152

153+
/// The string that must be at the start of a line in [_changeDescriptionFile]
154+
/// to allow skipping a version change for a PR that would normally require
155+
/// one.
156+
static const String _missingVersionChangeJustificationMarker =
157+
'No version change:';
158+
159+
/// The string that must be at the start of a line in [_changeDescriptionFile]
160+
/// to allow skipping a CHANGELOG change for a PR that would normally require
161+
/// one.
162+
static const String _missingChangelogChangeJustificationMarker =
163+
'No CHANGELOG change:';
164+
144165
final PubVersionFinder _pubVersionFinder;
145166

167+
late final GitVersionFinder _gitVersionFinder;
168+
late final String _mergeBase;
169+
late final List<String> _changedFiles;
170+
171+
late final String _changeDescription = _loadChangeDescription();
172+
146173
@override
147174
final String name = 'version-check';
148175

@@ -156,7 +183,11 @@ class VersionCheckCommand extends PackageLoopingCommand {
156183
bool get hasLongOutput => false;
157184

158185
@override
159-
Future<void> initializeRun() async {}
186+
Future<void> initializeRun() async {
187+
_gitVersionFinder = await retrieveVersionFinder();
188+
_mergeBase = await _gitVersionFinder.getBaseSha();
189+
_changedFiles = await _gitVersionFinder.getChangedFiles();
190+
}
160191

161192
@override
162193
Future<PackageResult> runForPackage(RepositoryPackage package) async {
@@ -206,6 +237,17 @@ class VersionCheckCommand extends PackageLoopingCommand {
206237
errors.add('CHANGELOG.md failed validation.');
207238
}
208239

240+
// If there are no other issues, make sure that there isn't a missing
241+
// change to the version and/or CHANGELOG.
242+
if (getBoolArg(_checkForMissingChanges) &&
243+
!versionChanged &&
244+
errors.isEmpty) {
245+
final String? error = await _checkForMissingChangeError(package);
246+
if (error != null) {
247+
errors.add(error);
248+
}
249+
}
250+
209251
return errors.isEmpty
210252
? PackageResult.success()
211253
: PackageResult.fail(errors);
@@ -239,18 +281,16 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body}
239281
}
240282

241283
/// Returns the version of [package] from git at the base comparison hash.
242-
Future<Version?> _getPreviousVersionFromGit(
243-
RepositoryPackage package, {
244-
required GitVersionFinder gitVersionFinder,
245-
}) async {
284+
Future<Version?> _getPreviousVersionFromGit(RepositoryPackage package) async {
246285
final File pubspecFile = package.pubspecFile;
247286
final String relativePath =
248287
path.relative(pubspecFile.absolute.path, from: (await gitDir).path);
249288
// Use Posix-style paths for git.
250289
final String gitPath = path.style == p.Style.windows
251290
? p.posix.joinAll(path.split(relativePath))
252291
: relativePath;
253-
return await gitVersionFinder.getPackageVersion(gitPath);
292+
return await _gitVersionFinder.getPackageVersion(gitPath,
293+
gitRef: _mergeBase);
254294
}
255295

256296
/// Returns the state of the verison of [package] relative to the comparison
@@ -274,11 +314,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body}
274314
'$indentation${pubspec.name}: Current largest version on pub: $previousVersion');
275315
}
276316
} else {
277-
final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
278-
previousVersionSource = await gitVersionFinder.getBaseSha();
279-
previousVersion = await _getPreviousVersionFromGit(package,
280-
gitVersionFinder: gitVersionFinder) ??
281-
Version.none;
317+
previousVersionSource = _mergeBase;
318+
previousVersion =
319+
await _getPreviousVersionFromGit(package) ?? Version.none;
282320
}
283321
if (previousVersion == Version.none) {
284322
print('${indentation}Unable to find previous version '
@@ -462,9 +500,11 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
462500
return false;
463501
}
464502

503+
String _getChangeDescription() => _changeDescription;
504+
465505
/// Returns the contents of the file pointed to by [_changeDescriptionFile],
466506
/// or an empty string if that flag is not provided.
467-
String _getChangeDescription() {
507+
String _loadChangeDescription() {
468508
final String path = getStringArg(_changeDescriptionFile);
469509
if (path.isEmpty) {
470510
return '';
@@ -476,4 +516,91 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
476516
}
477517
return file.readAsStringSync();
478518
}
519+
520+
/// Returns an error string if the changes to this package should have
521+
/// resulted in a version change, or shoud have resulted in a CHANGELOG change
522+
/// but didn't.
523+
///
524+
/// This should only be called if the version did not change.
525+
Future<String?> _checkForMissingChangeError(RepositoryPackage package) async {
526+
// Find the relative path to the current package, as it would appear at the
527+
// beginning of a path reported by getChangedFiles() (which always uses
528+
// Posix paths).
529+
final Directory gitRoot =
530+
packagesDir.fileSystem.directory((await gitDir).path);
531+
final String relativePackagePath =
532+
getRelativePosixPath(package.directory, from: gitRoot) + '/';
533+
bool hasChanges = false;
534+
bool needsVersionChange = false;
535+
bool hasChangelogChange = false;
536+
for (final String path in _changedFiles) {
537+
// Only consider files within the package.
538+
if (!path.startsWith(relativePackagePath)) {
539+
continue;
540+
}
541+
hasChanges = true;
542+
543+
final List<String> components = p.posix.split(path);
544+
final bool isChangelog = components.last == 'CHANGELOG.md';
545+
if (isChangelog) {
546+
hasChangelogChange = true;
547+
}
548+
549+
if (!needsVersionChange &&
550+
!isChangelog &&
551+
// The example's main.dart is shown on pub.dev, but for anything else
552+
// in the example publishing has no purpose.
553+
!(components.contains('example') && components.last != 'main.dart') &&
554+
// Changes to tests don't need to be published.
555+
!components.contains('test') &&
556+
!components.contains('androidTest') &&
557+
!components.contains('RunnerTests') &&
558+
!components.contains('RunnerUITests') &&
559+
// Ignoring lints doesn't affect clients.
560+
!components.contains('lint-baseline.xml')) {
561+
needsVersionChange = true;
562+
}
563+
}
564+
565+
if (!hasChanges) {
566+
return null;
567+
}
568+
569+
if (needsVersionChange) {
570+
if (_getChangeDescription().split('\n').any((String line) =>
571+
line.startsWith(_missingVersionChangeJustificationMarker))) {
572+
logWarning('Ignoring lack of version change due to '
573+
'"$_missingVersionChangeJustificationMarker" in the '
574+
'change description.');
575+
} else {
576+
printError(
577+
'No version change found, but the change to this package could '
578+
'not be verified to be exempt from version changes according to '
579+
'repository policy. If this is a false positive, please '
580+
'add a line starting with\n'
581+
'$_missingVersionChangeJustificationMarker\n'
582+
'to your PR description with an explanation of why it is exempt.');
583+
return 'Missing version change';
584+
}
585+
}
586+
587+
if (!hasChangelogChange) {
588+
if (_getChangeDescription().split('\n').any((String line) =>
589+
line.startsWith(_missingChangelogChangeJustificationMarker))) {
590+
logWarning('Ignoring lack of CHANGELOG update due to '
591+
'"$_missingChangelogChangeJustificationMarker" in the '
592+
'change description.');
593+
} else {
594+
printError(
595+
'No CHANGELOG change found. If this PR needs an exemption from'
596+
'the standard policy of listing all changes in the CHANGELOG, '
597+
'please add a line starting with\n'
598+
'$_missingChangelogChangeJustificationMarker\n'
599+
'to your PR description with an explanation of why.');
600+
return 'Missing CHANGELOG change';
601+
}
602+
}
603+
604+
return null;
605+
}
479606
}

script/tool/test/common/git_version_finder_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ void main() {
1414
late List<List<String>?> gitDirCommands;
1515
late String gitDiffResponse;
1616
late MockGitDir gitDir;
17-
String? mergeBaseResponse;
17+
String mergeBaseResponse = '';
1818

1919
setUp(() {
2020
gitDirCommands = <List<String>?>[];
@@ -74,7 +74,7 @@ file2/file2.cc
7474
final GitVersionFinder finder = GitVersionFinder(gitDir, null);
7575
await finder.getChangedFiles();
7676
verify(gitDir.runCommand(
77-
<String>['diff', '--name-only', mergeBaseResponse!, 'HEAD']));
77+
<String>['diff', '--name-only', mergeBaseResponse, 'HEAD']));
7878
});
7979

8080
test('use correct base sha if specified', () async {

0 commit comments

Comments
 (0)