Skip to content

Commit cdabfa9

Browse files
[flutter_plugin_tools] Allow overriding breaking change check (flutter#4369)
1 parent 8c5f0f0 commit cdabfa9

7 files changed

+188
-13
lines changed

.cirrus.yml

+15-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,21 @@ task:
6868
- cd script/tool
6969
- dart pub run test
7070
- name: publishable
71-
version_check_script: ./script/tool_runner.sh version-check
71+
env:
72+
CHANGE_DESC: "$TMPDIR/change-description.txt"
73+
version_check_script:
74+
# For pre-submit, pass the PR description to the script to allow for
75+
# platform version breaking version change justifications.
76+
# For post-submit, ignore platform version breaking version changes.
77+
# The PR description isn't reliably part of the commit message, so using
78+
# the same flags as for presubmit would likely result in false-positive
79+
# post-submit failures.
80+
- if [[ $CIRRUS_PR == "" ]]; then
81+
- ./script/tool_runner.sh version-check --ignore-platform-interface-breaks
82+
- else
83+
- echo "$CIRRUS_CHANGE_MESSAGE" > "$CHANGE_DESC"
84+
- ./script/tool_runner.sh version-check --change-description-file="$CHANGE_DESC"
85+
- fi
7286
publish_check_script: ./script/tool_runner.sh publish-check
7387
- name: format
7488
format_script: ./script/tool_runner.sh format --fail-on-change

script/tool/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
federated packages that have been done in such a way that they will pass in
77
CI, but fail once the change is landed and published.
88
- `publish-check` now validates that there is an `AUTHORS` file.
9+
- Added flags to `version-check` to allow overriding the platform interface
10+
major version change restriction.
911

1012
## 0.7.1
1113

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

+5
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ class RepositoryPackage {
5353
directory.parent.basename != 'packages' &&
5454
directory.basename.startsWith(directory.parent.basename);
5555

56+
/// True if this appears to be a platform interface package, according to
57+
/// repository conventions.
58+
bool get isPlatformInterface =>
59+
directory.basename.endsWith('_platform_interface');
60+
5661
/// Returns the Flutter example packages contained in the package, if any.
5762
Iterable<RepositoryPackage> getExamples() {
5863
final Directory exampleDirectory = directory.childDirectory('example');

script/tool/lib/src/drive_examples_command.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class DriveExamplesCommand extends PackageLoopingCommand {
133133

134134
@override
135135
Future<PackageResult> runForPackage(RepositoryPackage package) async {
136-
if (package.directory.basename.endsWith('_platform_interface') &&
136+
if (package.isPlatformInterface &&
137137
!package.getSingleExampleDeprecated().directory.existsSync()) {
138138
// Platform interface packages generally aren't intended to have
139139
// examples, and don't need integration tests, so skip rather than fail.

script/tool/lib/src/federation_safety_check_command.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand {
109109
return PackageResult.skip('Not a federated plugin.');
110110
}
111111

112-
if (package.directory.basename.endsWith('_platform_interface')) {
112+
if (package.isPlatformInterface) {
113113
// As the leaf nodes in the graph, a published package interface change is
114114
// assumed to be correct, and other changes are validated against that.
115115
return PackageResult.skip(

script/tool/lib/src/version_check_command.dart

+75-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import 'common/process_runner.dart';
1818
import 'common/pub_version_finder.dart';
1919
import 'common/repository_package.dart';
2020

21+
const int _exitMissingChangeDescriptionFile = 3;
22+
2123
/// Categories of version change types.
2224
enum NextVersionType {
2325
/// A breaking change.
@@ -108,13 +110,36 @@ class VersionCheckCommand extends PackageLoopingCommand {
108110
argParser.addFlag(
109111
_againstPubFlag,
110112
help: 'Whether the version check should run against the version on pub.\n'
111-
'Defaults to false, which means the version check only run against the previous version in code.',
113+
'Defaults to false, which means the version check only run against '
114+
'the previous version in code.',
112115
defaultsTo: false,
113116
negatable: true,
114117
);
118+
argParser.addOption(_changeDescriptionFile,
119+
help: 'The path to a file containing the description of the change '
120+
'(e.g., PR description or commit message).\n\n'
121+
'If supplied, this is used to allow overrides to some version '
122+
'checks.');
123+
argParser.addFlag(_ignorePlatformInterfaceBreaks,
124+
help: 'Bypasses the check that platform interfaces do not contain '
125+
'breaking changes.\n\n'
126+
'This is only intended for use in post-submit CI checks, to '
127+
'prevent the possibility of post-submit breakage if a change '
128+
'description justification is not transferred into the commit '
129+
'message. Pre-submit checks should always use '
130+
'--$_changeDescriptionFile instead.',
131+
hide: true);
115132
}
116133

117134
static const String _againstPubFlag = 'against-pub';
135+
static const String _changeDescriptionFile = 'change-description-file';
136+
static const String _ignorePlatformInterfaceBreaks =
137+
'ignore-platform-interface-breaks';
138+
139+
/// The string that must be in [_changeDescriptionFile] to allow a breaking
140+
/// change to a platform interface.
141+
static const String _breakingChangeJustificationMarker =
142+
'## Breaking change justification';
118143

119144
final PubVersionFinder _pubVersionFinder;
120145

@@ -292,16 +317,17 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body}
292317
return _CurrentVersionState.invalidChange;
293318
}
294319

295-
final bool isPlatformInterface =
296-
pubspec.name.endsWith('_platform_interface');
297-
// TODO(stuartmorgan): Relax this check. See
298-
// https://github.com/flutter/flutter/issues/85391
299-
if (isPlatformInterface &&
300-
allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR) {
320+
if (allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR &&
321+
!_validateBreakingChange(package)) {
301322
printError('${indentation}Breaking change detected.\n'
302-
'${indentation}Breaking changes to platform interfaces are strongly discouraged.\n');
323+
'${indentation}Breaking changes to platform interfaces are not '
324+
'allowed without explicit justification.\n'
325+
'${indentation}See '
326+
'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages '
327+
'for more information.');
303328
return _CurrentVersionState.invalidChange;
304329
}
330+
305331
return _CurrentVersionState.validChange;
306332
}
307333

@@ -398,4 +424,45 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog.
398424
return null;
399425
}
400426
}
427+
428+
/// Checks whether the current breaking change to [package] should be allowed,
429+
/// logging extra information for auditing when allowing unusual cases.
430+
bool _validateBreakingChange(RepositoryPackage package) {
431+
// Only platform interfaces have breaking change restrictions.
432+
if (!package.isPlatformInterface) {
433+
return true;
434+
}
435+
436+
if (getBoolArg(_ignorePlatformInterfaceBreaks)) {
437+
logWarning(
438+
'${indentation}Allowing breaking change to ${package.displayName} '
439+
'due to --$_ignorePlatformInterfaceBreaks');
440+
return true;
441+
}
442+
443+
if (_getChangeDescription().contains(_breakingChangeJustificationMarker)) {
444+
logWarning(
445+
'${indentation}Allowing breaking change to ${package.displayName} '
446+
'due to "$_breakingChangeJustificationMarker" in the change '
447+
'description.');
448+
return true;
449+
}
450+
451+
return false;
452+
}
453+
454+
/// Returns the contents of the file pointed to by [_changeDescriptionFile],
455+
/// or an empty string if that flag is not provided.
456+
String _getChangeDescription() {
457+
final String path = getStringArg(_changeDescriptionFile);
458+
if (path.isEmpty) {
459+
return '';
460+
}
461+
final File file = packagesDir.fileSystem.file(path);
462+
if (!file.existsSync()) {
463+
printError('${indentation}No such file: $path');
464+
throw ToolExit(_exitMissingChangeDescriptionFile);
465+
}
466+
return file.readAsStringSync();
467+
}
401468
}

script/tool/test/version_check_command_test.dart

+89-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class MockProcessResult extends Mock implements io.ProcessResult {}
4646
void main() {
4747
const String indentation = ' ';
4848
group('$VersionCheckCommand', () {
49-
FileSystem fileSystem;
49+
late FileSystem fileSystem;
5050
late MockPlatform mockPlatform;
5151
late Directory packagesDir;
5252
late CommandRunner<void> runner;
@@ -252,7 +252,8 @@ void main() {
252252
]));
253253
});
254254

255-
test('disallows breaking changes to platform interfaces', () async {
255+
test('disallows breaking changes to platform interfaces by default',
256+
() async {
256257
createFakePlugin('plugin_platform_interface', packagesDir,
257258
version: '2.0.0');
258259
gitShowResponses = <String, String>{
@@ -276,6 +277,92 @@ void main() {
276277
]));
277278
});
278279

280+
test('allows breaking changes to platform interfaces with explanation',
281+
() async {
282+
createFakePlugin('plugin_platform_interface', packagesDir,
283+
version: '2.0.0');
284+
gitShowResponses = <String, String>{
285+
'master:packages/plugin_platform_interface/pubspec.yaml':
286+
'version: 1.0.0',
287+
};
288+
final File changeDescriptionFile =
289+
fileSystem.file('change_description.txt');
290+
changeDescriptionFile.writeAsStringSync('''
291+
Some general PR description
292+
293+
## Breaking change justification
294+
295+
This is necessary because of X, Y, and Z
296+
297+
## Another section''');
298+
final List<String> output = await runCapturingPrint(runner, <String>[
299+
'version-check',
300+
'--base-sha=master',
301+
'--change-description-file=${changeDescriptionFile.path}'
302+
]);
303+
304+
expect(
305+
output,
306+
containsAllInOrder(<Matcher>[
307+
contains('Allowing breaking change to plugin_platform_interface '
308+
'due to "## Breaking change justification" in the change '
309+
'description.'),
310+
contains('Ran for 1 package(s) (1 with warnings)'),
311+
]),
312+
);
313+
});
314+
315+
test('throws if a nonexistent change description file is specified',
316+
() async {
317+
createFakePlugin('plugin_platform_interface', packagesDir,
318+
version: '2.0.0');
319+
gitShowResponses = <String, String>{
320+
'master:packages/plugin_platform_interface/pubspec.yaml':
321+
'version: 1.0.0',
322+
};
323+
324+
Error? commandError;
325+
final List<String> output = await runCapturingPrint(runner, <String>[
326+
'version-check',
327+
'--base-sha=master',
328+
'--change-description-file=a_missing_file.txt'
329+
], errorHandler: (Error e) {
330+
commandError = e;
331+
});
332+
333+
expect(commandError, isA<ToolExit>());
334+
expect(
335+
output,
336+
containsAllInOrder(<Matcher>[
337+
contains('No such file: a_missing_file.txt'),
338+
]),
339+
);
340+
});
341+
342+
test('allows breaking changes to platform interfaces with bypass flag',
343+
() async {
344+
createFakePlugin('plugin_platform_interface', packagesDir,
345+
version: '2.0.0');
346+
gitShowResponses = <String, String>{
347+
'master:packages/plugin_platform_interface/pubspec.yaml':
348+
'version: 1.0.0',
349+
};
350+
final List<String> output = await runCapturingPrint(runner, <String>[
351+
'version-check',
352+
'--base-sha=master',
353+
'--ignore-platform-interface-breaks'
354+
]);
355+
356+
expect(
357+
output,
358+
containsAllInOrder(<Matcher>[
359+
contains('Allowing breaking change to plugin_platform_interface due '
360+
'to --ignore-platform-interface-breaks'),
361+
contains('Ran for 1 package(s) (1 with warnings)'),
362+
]),
363+
);
364+
});
365+
279366
test('Allow empty lines in front of the first version in CHANGELOG',
280367
() async {
281368
const String version = '1.0.1';

0 commit comments

Comments
 (0)