[tools] Improves version-check logic (#6354)

Improves the logic used to determine whether to require a version and/or CHANGELOG change:
- Removes the requirement that dev-only (e.g., test) changes update the CHANGELOG, since in practice we were essentially always overriding in that case.
- Adds file-level analysis of `build.gradle` files to determine whether they are only changing test dependencies.
- Improves the "is this a published example file" logic to better match pub.dev's logic, to fix some false positives and false negatives (e.g., `rfw`'s `example/<foo>/lib/main.dart` being considered published).

Removes the no-longer-necessary special-case handling of some Dependabot PRs, as well as the PR-description-based system it was built on (and that turned out not to be very useful due to the way `CIRRUS_CHANGE_MESSAGE` actually worked). `build.gradle` analysis should not cover all such cases, and without the need to hard-code them by package name.
This commit is contained in:
stuartmorgan
2022-09-09 13:52:16 -04:00
committed by GitHub
parent edcd2662e6
commit 1aa2e82b56
8 changed files with 403 additions and 416 deletions

View File

@ -1,3 +1,13 @@
## 0.10.0
* Improves the logic in `version-check` to determine what changes don't require
version changes, as well as making any dev-only changes also not require
changelog changes since in practice we almost always override the check in
that case.
* Removes special-case handling of Dependabot PRs, and the (fragile)
`--change-description-file` flag was only still used for that case, as
the improved diff analysis now handles that case more robustly.
## 0.9.3 ## 0.9.3
* Raises minimum `compileSdkVersion` to 32 for the `all-plugins-app` command. * Raises minimum `compileSdkVersion` to 32 for the `all-plugins-app` command.

View File

@ -50,6 +50,27 @@ class GitVersionFinder {
return changedFiles.toList(); return changedFiles.toList();
} }
/// Get a list of all the changed files.
Future<List<String>> getDiffContents({
String? targetPath,
bool includeUncommitted = false,
}) async {
final String baseSha = await getBaseSha();
final io.ProcessResult diffCommand = await baseGitDir.runCommand(<String>[
'diff',
baseSha,
if (!includeUncommitted) 'HEAD',
if (targetPath != null) ...<String>['--', targetPath],
]);
final String diffStdout = diffCommand.stdout.toString();
if (diffStdout.isEmpty) {
return <String>[];
}
final List<String> changedFiles = diffStdout.split('\n')
..removeWhere((String element) => element.isEmpty);
return changedFiles.toList();
}
/// Get the package version specified in the pubspec file in `pubspecPath` and /// Get the package version specified in the pubspec file in `pubspecPath` and
/// at the revision of `gitRef` (defaulting to the base if not provided). /// at the revision of `gitRef` (defaulting to the base if not provided).
Future<Version?> getPackageVersion(String pubspecPath, Future<Version?> getPackageVersion(String pubspecPath,

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common/git_version_finder.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@ -14,6 +16,7 @@ class PackageChangeState {
const PackageChangeState({ const PackageChangeState({
required this.hasChanges, required this.hasChanges,
required this.hasChangelogChange, required this.hasChangelogChange,
required this.needsChangelogChange,
required this.needsVersionChange, required this.needsVersionChange,
}); });
@ -26,6 +29,10 @@ class PackageChangeState {
/// True if any changes in the package require a version change according /// True if any changes in the package require a version change according
/// to repository policy. /// to repository policy.
final bool needsVersionChange; final bool needsVersionChange;
/// True if any changes in the package require a CHANGELOG change according
/// to repository policy.
final bool needsChangelogChange;
} }
/// Checks [package] against [changedPaths] to determine what changes it has /// Checks [package] against [changedPaths] to determine what changes it has
@ -38,11 +45,15 @@ class PackageChangeState {
/// and `getRelativePosixPath(package.directory, gitDir.path)` respectively; /// and `getRelativePosixPath(package.directory, gitDir.path)` respectively;
/// they are arguments mainly to allow for caching the changed paths for an /// they are arguments mainly to allow for caching the changed paths for an
/// entire command run. /// entire command run.
PackageChangeState checkPackageChangeState( ///
/// If [git] is provided, [changedPaths] must be repository-relative
/// paths, and change type detection can use file diffs in addition to paths.
Future<PackageChangeState> checkPackageChangeState(
RepositoryPackage package, { RepositoryPackage package, {
required List<String> changedPaths, required List<String> changedPaths,
required String relativePackagePath, required String relativePackagePath,
}) { GitVersionFinder? git,
}) async {
final String packagePrefix = relativePackagePath.endsWith('/') final String packagePrefix = relativePackagePath.endsWith('/')
? relativePackagePath ? relativePackagePath
: '$relativePackagePath/'; : '$relativePackagePath/';
@ -50,6 +61,7 @@ PackageChangeState checkPackageChangeState(
bool hasChanges = false; bool hasChanges = false;
bool hasChangelogChange = false; bool hasChangelogChange = false;
bool needsVersionChange = false; bool needsVersionChange = false;
bool needsChangelogChange = false;
for (final String path in changedPaths) { for (final String path in changedPaths) {
// Only consider files within the package. // Only consider files within the package.
if (!path.startsWith(packagePrefix)) { if (!path.startsWith(packagePrefix)) {
@ -62,34 +74,131 @@ PackageChangeState checkPackageChangeState(
if (components.isEmpty) { if (components.isEmpty) {
continue; continue;
} }
final bool isChangelog = components.first == 'CHANGELOG.md';
if (isChangelog) { if (components.first == 'CHANGELOG.md') {
hasChangelogChange = true; hasChangelogChange = true;
continue;
} }
if (!needsVersionChange && if (!needsVersionChange) {
!isChangelog && // Developer-only changes don't need version changes or changelog changes.
// One of a few special files example will be shown on pub.dev, but for if (await _isDevChange(components, git: git, repoPath: path)) {
// anything else in the example publishing has no purpose. continue;
!(components.first == 'example' && }
!<String>{'main.dart', 'readme.md', 'example.md'}
.contains(components.last.toLowerCase())) && // Some other changes don't need version changes, but might benefit from
// Changes to tests don't need to be published. // changelog changes.
!components.contains('test') && needsChangelogChange = true;
!components.contains('androidTest') && if (
!components.contains('RunnerTests') && // One of a few special files example will be shown on pub.dev, but
!components.contains('RunnerUITests') && // for anything else in the example publishing has no purpose.
// The top-level "tool" directory is for non-client-facing utility code, !_isUnpublishedExampleChange(components, package)) {
// so doesn't need to be published. needsVersionChange = true;
components.first != 'tool' && }
// Ignoring lints doesn't affect clients.
!components.contains('lint-baseline.xml')) {
needsVersionChange = true;
} }
} }
return PackageChangeState( return PackageChangeState(
hasChanges: hasChanges, hasChanges: hasChanges,
hasChangelogChange: hasChangelogChange, hasChangelogChange: hasChangelogChange,
needsChangelogChange: needsChangelogChange,
needsVersionChange: needsVersionChange); needsVersionChange: needsVersionChange);
} }
bool _isTestChange(List<String> pathComponents) {
return pathComponents.contains('test') ||
pathComponents.contains('androidTest') ||
pathComponents.contains('RunnerTests') ||
pathComponents.contains('RunnerUITests');
}
// True if the given file is an example file other than the one that will be
// published according to https://dart.dev/tools/pub/package-layout#examples.
//
// This is not exhastive; it currently only handles variations we actually have
// in our repositories.
bool _isUnpublishedExampleChange(
List<String> pathComponents, RepositoryPackage package) {
if (pathComponents.first != 'example') {
return false;
}
final List<String> exampleComponents = pathComponents.sublist(1);
if (exampleComponents.isEmpty) {
return false;
}
final Directory exampleDirectory =
package.directory.childDirectory('example');
// Check for example.md/EXAMPLE.md first, as that has priority. If it's
// present, any other example file is unpublished.
final bool hasExampleMd =
exampleDirectory.childFile('example.md').existsSync() ||
exampleDirectory.childFile('EXAMPLE.md').existsSync();
if (hasExampleMd) {
return !(exampleComponents.length == 1 &&
exampleComponents.first.toLowerCase() == 'example.md');
}
// Most packages have an example/lib/main.dart (or occasionally
// example/main.dart), so check for that. The other naming variations aren't
// currently used.
const String mainName = 'main.dart';
final bool hasExampleCode =
exampleDirectory.childDirectory('lib').childFile(mainName).existsSync() ||
exampleDirectory.childFile(mainName).existsSync();
if (hasExampleCode) {
// If there is an example main, only that example file is published.
return !((exampleComponents.length == 1 &&
exampleComponents.first == mainName) ||
(exampleComponents.length == 2 &&
exampleComponents.first == 'lib' &&
exampleComponents[1] == mainName));
}
// If there's no example code either, the example README.md, if any, is the
// file that will be published.
return exampleComponents.first.toLowerCase() != 'readme.md';
}
// True if the change is only relevant to people working on the plugin.
Future<bool> _isDevChange(List<String> pathComponents,
{GitVersionFinder? git, String? repoPath}) async {
return _isTestChange(pathComponents) ||
// The top-level "tool" directory is for non-client-facing utility
// code, such as test scripts.
pathComponents.first == 'tool' ||
// Ignoring lints doesn't affect clients.
pathComponents.contains('lint-baseline.xml') ||
await _isGradleTestDependencyChange(pathComponents,
git: git, repoPath: repoPath);
}
Future<bool> _isGradleTestDependencyChange(List<String> pathComponents,
{GitVersionFinder? git, String? repoPath}) async {
if (git == null) {
return false;
}
if (pathComponents.last != 'build.gradle') {
return false;
}
final List<String> diff = await git.getDiffContents(targetPath: repoPath);
final RegExp changeLine = RegExp(r'[+-] ');
final RegExp testDependencyLine =
RegExp(r'[+-]\s*(?:androidT|t)estImplementation\s');
bool foundTestDependencyChange = false;
for (final String line in diff) {
if (!changeLine.hasMatch(line) ||
line.startsWith('--- ') ||
line.startsWith('+++ ')) {
continue;
}
if (!testDependencyLine.hasMatch(line)) {
return false;
}
foundTestDependencyChange = true;
}
// Only return true if a test dependency change was found, as a failsafe
// against having the wrong (e.g., incorrectly empty) diff output.
return foundTestDependencyChange;
}

View File

@ -133,7 +133,7 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand {
packagesDir.fileSystem.directory((await gitDir).path); packagesDir.fileSystem.directory((await gitDir).path);
final String relativePackagePath = final String relativePackagePath =
getRelativePosixPath(package.directory, from: gitRoot); getRelativePosixPath(package.directory, from: gitRoot);
final PackageChangeState state = checkPackageChangeState(package, final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: _changedFiles, changedPaths: _changedFiles,
relativePackagePath: relativePackagePath); relativePackagePath: relativePackagePath);

View File

@ -18,8 +18,6 @@ import 'common/process_runner.dart';
import 'common/pub_version_finder.dart'; import 'common/pub_version_finder.dart';
import 'common/repository_package.dart'; import 'common/repository_package.dart';
const int _exitMissingChangeDescriptionFile = 3;
/// Categories of version change types. /// Categories of version change types.
enum NextVersionType { enum NextVersionType {
/// A breaking change. /// A breaking change.
@ -116,11 +114,6 @@ class VersionCheckCommand extends PackageLoopingCommand {
'Defaults to false, which means the version check only run against ' 'Defaults to false, which means the version check only run against '
'the previous version in code.', 'the previous version in code.',
); );
argParser.addOption(_changeDescriptionFile,
help: 'The path to a file containing the description of the change '
'(e.g., PR description or commit message).\n\n'
'If supplied, this is used to allow overrides to some version '
'checks.');
argParser.addOption(_prLabelsArg, argParser.addOption(_prLabelsArg,
help: 'A comma-separated list of labels associated with this PR, ' help: 'A comma-separated list of labels associated with this PR, '
'if applicable.\n\n' 'if applicable.\n\n'
@ -144,7 +137,6 @@ class VersionCheckCommand extends PackageLoopingCommand {
} }
static const String _againstPubFlag = 'against-pub'; static const String _againstPubFlag = 'against-pub';
static const String _changeDescriptionFile = 'change-description-file';
static const String _prLabelsArg = 'pr-labels'; static const String _prLabelsArg = 'pr-labels';
static const String _checkForMissingChanges = 'check-for-missing-changes'; static const String _checkForMissingChanges = 'check-for-missing-changes';
static const String _ignorePlatformInterfaceBreaks = static const String _ignorePlatformInterfaceBreaks =
@ -171,7 +163,6 @@ class VersionCheckCommand extends PackageLoopingCommand {
late final String _mergeBase; late final String _mergeBase;
late final List<String> _changedFiles; late final List<String> _changedFiles;
late final String _changeDescription = _loadChangeDescription();
late final Set<String> _prLabels = _getPRLabels(); late final Set<String> _prLabels = _getPRLabels();
@override @override
@ -519,21 +510,6 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
return labels.split(',').map((String label) => label.trim()).toSet(); return labels.split(',').map((String label) => label.trim()).toSet();
} }
/// Returns the contents of the file pointed to by [_changeDescriptionFile],
/// or an empty string if that flag is not provided.
String _loadChangeDescription() {
final String path = getStringArg(_changeDescriptionFile);
if (path.isEmpty) {
return '';
}
final File file = packagesDir.fileSystem.file(path);
if (!file.existsSync()) {
printError('${indentation}No such file: $path');
throw ToolExit(_exitMissingChangeDescriptionFile);
}
return file.readAsStringSync();
}
/// Returns true if the given version transition should be allowed. /// Returns true if the given version transition should be allowed.
bool _shouldAllowVersionChange( bool _shouldAllowVersionChange(
{required Version oldVersion, required Version newVersion}) { {required Version oldVersion, required Version newVersion}) {
@ -569,8 +545,10 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
final String relativePackagePath = final String relativePackagePath =
getRelativePosixPath(package.directory, from: gitRoot); getRelativePosixPath(package.directory, from: gitRoot);
final PackageChangeState state = checkPackageChangeState(package, final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: _changedFiles, relativePackagePath: relativePackagePath); changedPaths: _changedFiles,
relativePackagePath: relativePackagePath,
git: await retrieveVersionFinder());
if (!state.hasChanges) { if (!state.hasChanges) {
return null; return null;
@ -580,9 +558,6 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
if (_prLabels.contains(_missingVersionChangeOverrideLabel)) { if (_prLabels.contains(_missingVersionChangeOverrideLabel)) {
logWarning('Ignoring lack of version change due to the ' logWarning('Ignoring lack of version change due to the '
'"$_missingVersionChangeOverrideLabel" label.'); '"$_missingVersionChangeOverrideLabel" label.');
} else if (_isAllowedDependabotChange(package, _changeDescription)) {
logWarning('Ignoring lack of version change for Dependabot change to '
'a known internal dependency.');
} else { } else {
printError( printError(
'No version change found, but the change to this package could ' 'No version change found, but the change to this package could '
@ -595,76 +570,23 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
} }
} }
if (!state.hasChangelogChange) { if (!state.hasChangelogChange && state.needsChangelogChange) {
if (_prLabels.contains(_missingChangelogChangeOverrideLabel)) { if (_prLabels.contains(_missingChangelogChangeOverrideLabel)) {
logWarning('Ignoring lack of CHANGELOG update due to the ' logWarning('Ignoring lack of CHANGELOG update due to the '
'"$_missingChangelogChangeOverrideLabel" label.'); '"$_missingChangelogChangeOverrideLabel" label.');
} else if (_isAllowedDependabotChange(package, _changeDescription)) {
logWarning('Ignoring lack of CHANGELOG update for Dependabot change to '
'a known internal dependency.');
} else { } else {
printError( printError(
'No CHANGELOG change found. If this PR needs an exemption from ' 'No CHANGELOG change found. If this PR needs an exemption from '
'the standard policy of listing all changes in the CHANGELOG, ' 'the standard policy of listing all changes in the CHANGELOG, '
'comment in the PR to explain why the PR is exempt, and add (or ' 'comment in the PR to explain why the PR is exempt, and add (or '
'ask your reviewer to add) the ' 'ask your reviewer to add) the '
'"$_missingChangelogChangeOverrideLabel" label.'); '"$_missingChangelogChangeOverrideLabel" label. Otherwise, '
'please add a NEXT entry in the CHANGELOG as described in '
'the contributing guide.');
return 'Missing CHANGELOG change'; return 'Missing CHANGELOG change';
} }
} }
return null; return null;
} }
/// Returns true if [changeDescription] matches a Dependabot change for a
/// dependency roll that should bypass the normal version and CHANGELOG change
/// checks (for dependencies that are known not to have client impact).
///
/// Depending on CI, [changeDescription] may either be the PR description, or
/// the description of the last commit (see for example discussion in
/// https://github.com/cirruslabs/cirrus-ci-docs/issues/1029), so this needs
/// to handle both.
bool _isAllowedDependabotChange(
RepositoryPackage package, String changeDescription) {
// Espresso exports some dependencies that are normally just internal test
// utils, so always require reviewers to check that.
if (package.directory.basename == 'espresso') {
return false;
}
// A string that is in all Dependabot PRs, but extremely unlikely to be in
// any other PR, to identify Dependabot PRs.
const String dependabotPRDescriptionMarker =
'Dependabot commands and options';
// The same thing, but for the Dependabot commit message.
const String dependabotCommitMessageMarker =
'Signed-off-by: dependabot[bot]';
// Expression to extract the name of the dependency being updated.
final RegExp dependencyRegex =
RegExp(r'Bumps? \[(.*?)\]\(.*?\) from [\d.]+ to [\d.]+');
// Allowed exact dependency names.
const Set<String> allowedDependencies = <String>{
'junit',
'robolectric',
};
const Set<String> allowedDependencyPrefixes = <String>{
'mockito-' // mockito-core, mockito-inline, etc.
};
if (changeDescription.contains(dependabotPRDescriptionMarker) ||
changeDescription.contains(dependabotCommitMessageMarker)) {
final Match? match = dependencyRegex.firstMatch(changeDescription);
if (match != null) {
final String dependency = match.group(1)!;
if (allowedDependencies.contains(dependency) ||
allowedDependencyPrefixes
.any((String prefix) => dependency.startsWith(prefix))) {
return true;
}
}
}
return false;
}
} }

View File

@ -1,7 +1,7 @@
name: flutter_plugin_tools name: flutter_plugin_tools
description: Productivity utils for flutter/plugins and flutter/packages description: Productivity utils for flutter/plugins and flutter/packages
repository: https://github.com/flutter/plugins/tree/main/script/tool repository: https://github.com/flutter/plugins/tree/main/script/tool
version: 0.9.3 version: 0.10.0
dependencies: dependencies:
args: ^2.1.0 args: ^2.1.0

View File

@ -3,7 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_plugin_tools/src/common/git_version_finder.dart';
import 'package:flutter_plugin_tools/src/common/package_state_utils.dart'; import 'package:flutter_plugin_tools/src/common/package_state_utils.dart';
import 'package:test/fake.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../util.dart'; import '../util.dart';
@ -26,12 +28,13 @@ void main() {
'packages/a_package/lib/plugin.dart', 'packages/a_package/lib/plugin.dart',
]; ];
final PackageChangeState state = checkPackageChangeState(package, final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles, changedPaths: changedFiles,
relativePackagePath: 'packages/a_package'); relativePackagePath: 'packages/a_package');
expect(state.hasChanges, true); expect(state.hasChanges, true);
expect(state.needsVersionChange, true); expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
}); });
test('handles trailing slash on package path', () async { test('handles trailing slash on package path', () async {
@ -42,16 +45,18 @@ void main() {
'packages/a_package/lib/plugin.dart', 'packages/a_package/lib/plugin.dart',
]; ];
final PackageChangeState state = checkPackageChangeState(package, final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles, changedPaths: changedFiles,
relativePackagePath: 'packages/a_package/'); relativePackagePath: 'packages/a_package/');
expect(state.hasChanges, true); expect(state.hasChanges, true);
expect(state.needsVersionChange, true); expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
expect(state.hasChangelogChange, false); expect(state.hasChangelogChange, false);
}); });
test('does not report version change exempt changes', () async { test('does not flag version- and changelog-change-exempt changes',
() async {
final RepositoryPackage package = final RepositoryPackage package =
createFakePlugin('a_plugin', packagesDir); createFakePlugin('a_plugin', packagesDir);
@ -64,12 +69,13 @@ void main() {
'packages/a_plugin/CHANGELOG.md', 'packages/a_plugin/CHANGELOG.md',
]; ];
final PackageChangeState state = checkPackageChangeState(package, final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles, changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/'); relativePackagePath: 'packages/a_plugin/');
expect(state.hasChanges, true); expect(state.hasChanges, true);
expect(state.needsVersionChange, false); expect(state.needsVersionChange, false);
expect(state.needsChangelogChange, false);
expect(state.hasChangelogChange, true); expect(state.hasChangelogChange, true);
}); });
@ -81,28 +87,49 @@ void main() {
'packages/a_plugin/lib/foo/tool/tool_thing.dart', 'packages/a_plugin/lib/foo/tool/tool_thing.dart',
]; ];
final PackageChangeState state = checkPackageChangeState(package, final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles, changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/'); relativePackagePath: 'packages/a_plugin/');
expect(state.hasChanges, true); expect(state.hasChanges, true);
expect(state.needsVersionChange, true); expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
}); });
test('requires a version change for example main', () async { test('requires a version change for example/lib/main.dart', () async {
final RepositoryPackage package = final RepositoryPackage package = createFakePlugin(
createFakePlugin('a_plugin', packagesDir); 'a_plugin', packagesDir,
extraFiles: <String>['example/lib/main.dart']);
const List<String> changedFiles = <String>[ const List<String> changedFiles = <String>[
'packages/a_plugin/example/lib/main.dart', 'packages/a_plugin/example/lib/main.dart',
]; ];
final PackageChangeState state = checkPackageChangeState(package, final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles, changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/'); relativePackagePath: 'packages/a_plugin/');
expect(state.hasChanges, true); expect(state.hasChanges, true);
expect(state.needsVersionChange, true); expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
});
test('requires a version change for example/main.dart', () async {
final RepositoryPackage package = createFakePlugin(
'a_plugin', packagesDir,
extraFiles: <String>['example/main.dart']);
const List<String> changedFiles = <String>[
'packages/a_plugin/example/main.dart',
];
final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/');
expect(state.hasChanges, true);
expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
}); });
test('requires a version change for example readme.md', () async { test('requires a version change for example readme.md', () async {
@ -113,28 +140,189 @@ void main() {
'packages/a_plugin/example/README.md', 'packages/a_plugin/example/README.md',
]; ];
final PackageChangeState state = checkPackageChangeState(package, final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles, changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/'); relativePackagePath: 'packages/a_plugin/');
expect(state.hasChanges, true); expect(state.hasChanges, true);
expect(state.needsVersionChange, true); expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
}); });
test('requires a version change for example example.md', () async { test('requires a version change for example/example.md', () async {
final RepositoryPackage package = createFakePlugin(
'a_plugin', packagesDir,
extraFiles: <String>['example/example.md']);
const List<String> changedFiles = <String>[
'packages/a_plugin/example/example.md',
];
final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/');
expect(state.hasChanges, true);
expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
});
test(
'requires a changelog change but no version change for '
'lower-priority examples when example.md is present', () async {
final RepositoryPackage package = createFakePlugin(
'a_plugin', packagesDir,
extraFiles: <String>['example/example.md']);
const List<String> changedFiles = <String>[
'packages/a_plugin/example/lib/main.dart',
'packages/a_plugin/example/main.dart',
'packages/a_plugin/example/README.md',
];
final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/');
expect(state.hasChanges, true);
expect(state.needsVersionChange, false);
expect(state.needsChangelogChange, true);
});
test(
'requires a changelog change but no version change for README.md when '
'code example is present', () async {
final RepositoryPackage package = createFakePlugin(
'a_plugin', packagesDir,
extraFiles: <String>['example/lib/main.dart']);
const List<String> changedFiles = <String>[
'packages/a_plugin/example/README.md',
];
final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/');
expect(state.hasChanges, true);
expect(state.needsVersionChange, false);
expect(state.needsChangelogChange, true);
});
test(
'does not requires changelog or version change for build.gradle '
'test-dependency-only changes', () async {
final RepositoryPackage package = final RepositoryPackage package =
createFakePlugin('a_plugin', packagesDir); createFakePlugin('a_plugin', packagesDir);
const List<String> changedFiles = <String>[ const List<String> changedFiles = <String>[
'packages/a_plugin/example/lib/example.md', 'packages/a_plugin/android/build.gradle',
]; ];
final PackageChangeState state = checkPackageChangeState(package, final GitVersionFinder git = FakeGitVersionFinder(<String, List<String>>{
'packages/a_plugin/android/build.gradle': <String>[
"- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'",
"- testImplementation 'junit:junit:4.10.0'",
"+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'",
"+ testImplementation 'junit:junit:4.13.2'",
]
});
final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/',
git: git);
expect(state.hasChanges, true);
expect(state.needsVersionChange, false);
expect(state.needsChangelogChange, false);
});
test('requires changelog or version change for other build.gradle changes',
() async {
final RepositoryPackage package =
createFakePlugin('a_plugin', packagesDir);
const List<String> changedFiles = <String>[
'packages/a_plugin/android/build.gradle',
];
final GitVersionFinder git = FakeGitVersionFinder(<String, List<String>>{
'packages/a_plugin/android/build.gradle': <String>[
"- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'",
"- testImplementation 'junit:junit:4.10.0'",
"+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'",
"+ testImplementation 'junit:junit:4.13.2'",
"- implementation 'com.google.android.gms:play-services-maps:18.0.0'",
"+ implementation 'com.google.android.gms:play-services-maps:18.0.2'",
]
});
final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/',
git: git);
expect(state.hasChanges, true);
expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
});
test(
'requires changelog or version change if build.gradle diffs cannot '
'be checked', () async {
final RepositoryPackage package =
createFakePlugin('a_plugin', packagesDir);
const List<String> changedFiles = <String>[
'packages/a_plugin/android/build.gradle',
];
final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles, changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/'); relativePackagePath: 'packages/a_plugin/');
expect(state.hasChanges, true); expect(state.hasChanges, true);
expect(state.needsVersionChange, true); expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
});
test(
'requires changelog or version change if build.gradle diffs cannot '
'be determined', () async {
final RepositoryPackage package =
createFakePlugin('a_plugin', packagesDir);
const List<String> changedFiles = <String>[
'packages/a_plugin/android/build.gradle',
];
final GitVersionFinder git = FakeGitVersionFinder(<String, List<String>>{
'packages/a_plugin/android/build.gradle': <String>[]
});
final PackageChangeState state = await checkPackageChangeState(package,
changedPaths: changedFiles,
relativePackagePath: 'packages/a_plugin/',
git: git);
expect(state.hasChanges, true);
expect(state.needsVersionChange, true);
expect(state.needsChangelogChange, true);
}); });
}); });
} }
class FakeGitVersionFinder extends Fake implements GitVersionFinder {
FakeGitVersionFinder(this.fileDiffs);
final Map<String, List<String>> fileDiffs;
@override
Future<List<String>> getDiffContents({
String? targetPath,
bool includeUncommitted = false,
}) async {
return fileDiffs[targetPath]!;
}
}

View File

@ -40,72 +40,6 @@ void testAllowedVersion(
} }
} }
String _generateFakeDependabotPRDescription(String package) {
return '''
Bumps [$package](https://github.com/foo/$package) from 1.0.0 to 2.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/foo/$package">$package's releases</a>.</em></p>
<blockquote>
...
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>...</li>
</ul>
</details>
<br />
[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=$package&package-manager=gradle&previous-version=1.0.0&new-version=2.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
</details>
''';
}
String _generateFakeDependabotCommitMessage(String package) {
return '''
Bumps [$package](https://github.com/foo/$package) from 1.0.0 to 2.0.0.
- [Release notes](https://github.com/foo/$package/releases)
- [Commits](foo/$package@v4.3.1...v4.6.1)
---
updated-dependencies:
- dependency-name: $package
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
''';
}
class MockProcessResult extends Mock implements io.ProcessResult {} class MockProcessResult extends Mock implements io.ProcessResult {}
void main() { void main() {
@ -1085,242 +1019,45 @@ packages/plugin/example/lib/foo.dart
); );
}); });
group('dependabot', () { // This test ensures that Dependabot Gradle changes to test-only files
test('throws if a nonexistent change description file is specified', // aren't flagged by the version check.
() async { test(
final RepositoryPackage plugin = 'allows missing CHANGELOG and version change for test-only Gradle changes',
createFakePlugin('plugin', packagesDir, version: '1.0.0'); () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, version: '1.0.0');
const String changelog = ''' const String changelog = '''
## 1.0.0 ## 1.0.0
* Some changes. * Some changes.
'''; ''';
plugin.changelogFile.writeAsStringSync(changelog); plugin.changelogFile.writeAsStringSync(changelog);
processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[ processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
MockProcess(stdout: 'version: 1.0.0'), MockProcess(stdout: 'version: 1.0.0'),
]; ];
processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[ processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
MockProcess(stdout: ''' // File list.
MockProcess(stdout: '''
packages/plugin/android/build.gradle packages/plugin/android/build.gradle
'''), '''),
]; // build.gradle diff
MockProcess(stdout: '''
Error? commandError; - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
final List<String> output = await _runWithMissingChangeDetection( - testImplementation 'junit:junit:4.10.0'
<String>['--change-description-file=a_missing_file.txt'], + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
errorHandler: (Error e) { + testImplementation 'junit:junit:4.13.2'
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('No such file: a_missing_file.txt'),
]),
);
});
test('allows missing version and CHANGELOG change for mockito',
() async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, version: '1.0.0');
const String changelog = '''
## 1.0.0
* Some changes.
''';
plugin.changelogFile.writeAsStringSync(changelog);
processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
MockProcess(stdout: 'version: 1.0.0'),
];
processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
MockProcess(stdout: '''
packages/plugin/android/build.gradle
'''), '''),
]; ];
final File changeDescriptionFile = final List<String> output =
fileSystem.file('change_description.txt'); await _runWithMissingChangeDetection(<String>[]);
changeDescriptionFile.writeAsStringSync(
_generateFakeDependabotPRDescription('mockito-core'));
final List<String> output = expect(
await _runWithMissingChangeDetection(<String>[ output,
'--change-description-file=${changeDescriptionFile.path}' containsAllInOrder(<Matcher>[
]); contains('Running for plugin'),
]),
expect( );
output,
containsAllInOrder(<Matcher>[
contains('Ignoring lack of version change for Dependabot '
'change to a known internal dependency.'),
contains('Ignoring lack of CHANGELOG update for Dependabot '
'change to a known internal dependency.'),
]),
);
});
test('allows missing version and CHANGELOG change for robolectric',
() async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, version: '1.0.0');
const String changelog = '''
## 1.0.0
* Some changes.
''';
plugin.changelogFile.writeAsStringSync(changelog);
processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
MockProcess(stdout: 'version: 1.0.0'),
];
processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
MockProcess(stdout: '''
packages/plugin/android/build.gradle
'''),
];
final File changeDescriptionFile =
fileSystem.file('change_description.txt');
changeDescriptionFile.writeAsStringSync(
_generateFakeDependabotPRDescription('robolectric'));
final List<String> output =
await _runWithMissingChangeDetection(<String>[
'--change-description-file=${changeDescriptionFile.path}'
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Ignoring lack of version change for Dependabot '
'change to a known internal dependency.'),
contains('Ignoring lack of CHANGELOG update for Dependabot '
'change to a known internal dependency.'),
]),
);
});
test('allows missing version and CHANGELOG change for junit', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, version: '1.0.0');
const String changelog = '''
## 1.0.0
* Some changes.
''';
plugin.changelogFile.writeAsStringSync(changelog);
processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
MockProcess(stdout: 'version: 1.0.0'),
];
processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
MockProcess(stdout: '''
packages/plugin/android/build.gradle
'''),
];
final File changeDescriptionFile =
fileSystem.file('change_description.txt');
changeDescriptionFile
.writeAsStringSync(_generateFakeDependabotPRDescription('junit'));
final List<String> output =
await _runWithMissingChangeDetection(<String>[
'--change-description-file=${changeDescriptionFile.path}'
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Ignoring lack of version change for Dependabot '
'change to a known internal dependency.'),
contains('Ignoring lack of CHANGELOG update for Dependabot '
'change to a known internal dependency.'),
]),
);
});
test('fails for dependencies that are not explicitly allowed',
() async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, version: '1.0.0');
const String changelog = '''
## 1.0.0
* Some changes.
''';
plugin.changelogFile.writeAsStringSync(changelog);
processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
MockProcess(stdout: 'version: 1.0.0'),
];
processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
MockProcess(stdout: '''
packages/plugin/android/build.gradle
'''),
];
final File changeDescriptionFile =
fileSystem.file('change_description.txt');
changeDescriptionFile.writeAsStringSync(
_generateFakeDependabotPRDescription('somethingelse'));
Error? commandError;
final List<String> output =
await _runWithMissingChangeDetection(<String>[
'--change-description-file=${changeDescriptionFile.path}'
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('No version change found'),
contains('plugin:\n'
' Missing version change'),
]),
);
});
test('allow list works for commit messages', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, version: '1.0.0');
const String changelog = '''
## 1.0.0
* Some changes.
''';
plugin.changelogFile.writeAsStringSync(changelog);
processRunner.mockProcessesForExecutable['git-show'] = <io.Process>[
MockProcess(stdout: 'version: 1.0.0'),
];
processRunner.mockProcessesForExecutable['git-diff'] = <io.Process>[
MockProcess(stdout: '''
packages/plugin/android/build.gradle
'''),
];
final File changeDescriptionFile =
fileSystem.file('change_description.txt');
changeDescriptionFile.writeAsStringSync(
_generateFakeDependabotCommitMessage('mockito-core'));
final List<String> output =
await _runWithMissingChangeDetection(<String>[
'--change-description-file=${changeDescriptionFile.path}'
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Ignoring lack of version change for Dependabot '
'change to a known internal dependency.'),
contains('Ignoring lack of CHANGELOG update for Dependabot '
'change to a known internal dependency.'),
]),
);
});
}); });
}); });