[tool] Bypass version/changelog checks for some PRs (#6124)

This commit is contained in:
stuartmorgan
2022-07-20 16:15:04 -04:00
committed by GitHub
parent 3158ec8daa
commit 223a080ad6
3 changed files with 270 additions and 2 deletions

View File

@ -1,3 +1,8 @@
## NEXT
- Bypasses version and CHANGELOG checks for Dependabot PRs for packages
that are known not to be client-affecting.
## 0.8.8 ## 0.8.8
- Allows pre-release versions in `version-check`. - Allows pre-release versions in `version-check`.

View File

@ -569,11 +569,15 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
} }
if (state.needsVersionChange) { if (state.needsVersionChange) {
if (_getChangeDescription().split('\n').any((String line) => final String changeDescription = _getChangeDescription();
if (changeDescription.split('\n').any((String line) =>
line.startsWith(_missingVersionChangeJustificationMarker))) { line.startsWith(_missingVersionChangeJustificationMarker))) {
logWarning('Ignoring lack of version change due to ' logWarning('Ignoring lack of version change due to '
'"$_missingVersionChangeJustificationMarker" in the ' '"$_missingVersionChangeJustificationMarker" in the '
'change description.'); 'change description.');
} 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 '
@ -587,11 +591,15 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
} }
if (!state.hasChangelogChange) { if (!state.hasChangelogChange) {
if (_getChangeDescription().split('\n').any((String line) => final String changeDescription = _getChangeDescription();
if (changeDescription.split('\n').any((String line) =>
line.startsWith(_missingChangelogChangeJustificationMarker))) { line.startsWith(_missingChangelogChangeJustificationMarker))) {
logWarning('Ignoring lack of CHANGELOG update due to ' logWarning('Ignoring lack of CHANGELOG update due to '
'"$_missingChangelogChangeJustificationMarker" in the ' '"$_missingChangelogChangeJustificationMarker" in the '
'change description.'); 'change description.');
} 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 '
@ -605,4 +613,46 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
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).
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 dependabotMarker = 'Dependabot commands and options';
// 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(dependabotMarker)) {
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

@ -40,6 +40,55 @@ 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>
''';
}
class MockProcessResult extends Mock implements io.ProcessResult {} class MockProcessResult extends Mock implements io.ProcessResult {}
void main() { void main() {
@ -1078,6 +1127,170 @@ No CHANGELOG change: Code change is only to implementation comments.
]), ]),
); );
}); });
group('dependabot', () {
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 =
fileSystem.file('change_description.txt');
changeDescriptionFile.writeAsStringSync(
_generateFakeDependabotPRDescription('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.'),
]),
);
});
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('allows valid against pub', () async { test('allows valid against pub', () async {