Skip to content

Commit 50238e7

Browse files
[tool] Add a package-level pre-publish hook (flutter#7156)
Adds the ability for a package to specify a script that should be run before publishing. To minimize the chance of such a script breaking things only in the post-submit `release` step, if the script is present it will also be run during `publish-check`. These should be used with caution since they can cause the published artifacts to be different from that is checked in, but in the intended use case of extension builds this risk is far preferable to the risks associated with checking in binaries that were built on local, ad-hoc basis. (Longer term, we may need an alternate solution however, as generating artifacts in CI can have its own supply chain validation issues.) Also does some minor refactoring to custom test script code to make it follow the same pattern as this new code. Fixes flutter#150210
1 parent 94f8b2f commit 50238e7

File tree

6 files changed

+244
-7
lines changed

6 files changed

+244
-7
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ class RepositoryPackage {
6666
/// The test directory containing the package's Dart tests.
6767
Directory get testDirectory => directory.childDirectory('test');
6868

69+
/// The path to the script that is run by the `custom-test` command.
70+
File get customTestScript =>
71+
directory.childDirectory('tool').childFile('run_tests.dart');
72+
73+
/// The path to the script that is run before publishing.
74+
File get prePublishScript =>
75+
directory.childDirectory('tool').childFile('pre_publish.dart');
76+
6977
/// Returns the directory containing support for [platform].
7078
Directory platformDirectory(FlutterPlatform platform) {
7179
late final String directoryName;

script/tool/lib/src/custom_test_command.dart

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import 'common/package_looping_command.dart';
88
import 'common/pub_utils.dart';
99
import 'common/repository_package.dart';
1010

11-
const String _scriptName = 'run_tests.dart';
1211
const String _legacyScriptName = 'run_tests.sh';
1312

1413
/// A command to run custom, package-local tests on packages.
@@ -32,13 +31,14 @@ class CustomTestCommand extends PackageLoopingCommand {
3231

3332
@override
3433
final String description = 'Runs package-specific custom tests defined in '
35-
"a package's tool/$_scriptName file.\n\n"
34+
"a package's custom test script.\n\n"
3635
'This command requires "dart" to be in your path.';
3736

3837
@override
3938
Future<PackageResult> runForPackage(RepositoryPackage package) async {
40-
final File script =
41-
package.directory.childDirectory('tool').childFile(_scriptName);
39+
final File script = package.customTestScript;
40+
final String relativeScriptPath =
41+
getRelativePosixPath(script, from: package.directory);
4242
final File legacyScript = package.directory.childFile(_legacyScriptName);
4343
String? customSkipReason;
4444
bool ranTests = false;
@@ -52,7 +52,7 @@ class CustomTestCommand extends PackageLoopingCommand {
5252
}
5353

5454
final int testExitCode = await processRunner.runAndStream(
55-
'dart', <String>['run', 'tool/$_scriptName'],
55+
'dart', <String>['run', relativeScriptPath],
5656
workingDir: package.directory);
5757
if (testExitCode != 0) {
5858
return PackageResult.fail();
@@ -64,7 +64,7 @@ class CustomTestCommand extends PackageLoopingCommand {
6464
if (legacyScript.existsSync()) {
6565
if (platform.isWindows) {
6666
customSkipReason = '$_legacyScriptName is not supported on Windows. '
67-
'Please migrate to $_scriptName.';
67+
'Please migrate to $relativeScriptPath.';
6868
} else {
6969
final int exitCode = await processRunner.runAndStream(
7070
legacyScript.path, <String>[],
@@ -77,7 +77,8 @@ class CustomTestCommand extends PackageLoopingCommand {
7777
}
7878

7979
if (!ranTests) {
80-
return PackageResult.skip(customSkipReason ?? 'No custom tests');
80+
return PackageResult.skip(
81+
customSkipReason ?? 'No $relativeScriptPath file');
8182
}
8283

8384
return PackageResult.success();

script/tool/lib/src/publish_check_command.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'dart:convert';
77
import 'dart:io' as io;
88

9+
import 'package:file/file.dart';
910
import 'package:http/http.dart' as http;
1011
import 'package:pub_semver/pub_semver.dart';
1112

@@ -83,6 +84,9 @@ class PublishCheckCommand extends PackageLoopingCommand {
8384
isError: true);
8485
result = _PublishCheckResult.error;
8586
}
87+
if (!await _validatePrePublishHook(package)) {
88+
result = _PublishCheckResult.error;
89+
}
8690

8791
if (result.index > _overallResult.index) {
8892
_overallResult = result;
@@ -268,6 +272,33 @@ HTTP response: ${pubVersionFinderResponse.httpResponse.body}
268272
return package.authorsFile.existsSync();
269273
}
270274

275+
Future<bool> _validatePrePublishHook(RepositoryPackage package) async {
276+
final File script = package.prePublishScript;
277+
if (!script.existsSync()) {
278+
// If there's no custom step, then it can't block publishing.
279+
return true;
280+
}
281+
final String relativeScriptPath =
282+
getRelativePosixPath(script, from: package.directory);
283+
print('Running pre-publish hook $relativeScriptPath...');
284+
285+
// Ensure that dependencies are available.
286+
if (!await runPubGet(package, processRunner, platform)) {
287+
_printImportantStatusMessage('Failed to get depenedencies',
288+
isError: true);
289+
return false;
290+
}
291+
292+
final int exitCode = await processRunner.runAndStream(
293+
'dart', <String>['run', relativeScriptPath],
294+
workingDir: package.directory);
295+
if (exitCode != 0) {
296+
_printImportantStatusMessage('Pre-publish script failed.', isError: true);
297+
return false;
298+
}
299+
return true;
300+
}
301+
271302
void _printImportantStatusMessage(String message, {required bool isError}) {
272303
final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message';
273304
if (getBoolArg(_machineFlag)) {

script/tool/lib/src/publish_command.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'common/git_version_finder.dart';
2121
import 'common/output_utils.dart';
2222
import 'common/package_command.dart';
2323
import 'common/package_looping_command.dart';
24+
import 'common/pub_utils.dart';
2425
import 'common/pub_version_finder.dart';
2526
import 'common/repository_package.dart';
2627

@@ -201,6 +202,10 @@ class PublishCommand extends PackageLoopingCommand {
201202
return checkResult;
202203
}
203204

205+
if (!await _runPrePublishScript(package)) {
206+
return PackageResult.fail(<String>['pre-publish failed']);
207+
}
208+
204209
if (!await _checkGitStatus(package)) {
205210
return PackageResult.fail(<String>['uncommitted changes']);
206211
}
@@ -375,6 +380,31 @@ Safe to ignore if the package is deleted in this commit.
375380
return getRemoteUrlResult.stdout as String?;
376381
}
377382

383+
Future<bool> _runPrePublishScript(RepositoryPackage package) async {
384+
final File script = package.prePublishScript;
385+
if (!script.existsSync()) {
386+
return true;
387+
}
388+
final String relativeScriptPath =
389+
getRelativePosixPath(script, from: package.directory);
390+
print('Running pre-publish hook $relativeScriptPath...');
391+
392+
// Ensure that dependencies are available.
393+
if (!await runPubGet(package, processRunner, platform)) {
394+
printError('Failed to get depenedencies');
395+
return false;
396+
}
397+
398+
final int exitCode = await processRunner.runAndStream(
399+
'dart', <String>['run', relativeScriptPath],
400+
workingDir: package.directory);
401+
if (exitCode != 0) {
402+
printError('Pre-publish script failed.');
403+
return false;
404+
}
405+
return true;
406+
}
407+
378408
Future<bool> _publish(RepositoryPackage package) async {
379409
print('Publishing...');
380410
print('Running `pub publish ${_publishFlags.join(' ')}` in '

script/tool/test/publish_check_command_test.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,88 @@ void main() {
369369
));
370370
});
371371

372+
group('pre-publish script', () {
373+
test('runs if present', () async {
374+
final RepositoryPackage package =
375+
createFakePackage('a_package', packagesDir, examples: <String>[]);
376+
package.prePublishScript.createSync(recursive: true);
377+
378+
final List<String> output = await runCapturingPrint(runner, <String>[
379+
'publish-check',
380+
]);
381+
382+
expect(
383+
output,
384+
containsAllInOrder(<Matcher>[
385+
contains('Running pre-publish hook tool/pre_publish.dart...'),
386+
]),
387+
);
388+
expect(
389+
processRunner.recordedCalls,
390+
containsAllInOrder(<ProcessCall>[
391+
ProcessCall(
392+
'dart',
393+
const <String>[
394+
'pub',
395+
'get',
396+
],
397+
package.directory.path),
398+
ProcessCall(
399+
'dart',
400+
const <String>[
401+
'run',
402+
'tool/pre_publish.dart',
403+
],
404+
package.directory.path),
405+
]));
406+
});
407+
408+
test('causes command failure if it fails', () async {
409+
final RepositoryPackage package = createFakePackage(
410+
'a_package', packagesDir,
411+
isFlutter: true, examples: <String>[]);
412+
package.prePublishScript.createSync(recursive: true);
413+
414+
processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
415+
FakeProcessInfo(MockProcess(exitCode: 1),
416+
<String>['run']), // run tool/pre_publish.dart
417+
];
418+
419+
Error? commandError;
420+
final List<String> output = await runCapturingPrint(runner, <String>[
421+
'publish-check',
422+
], errorHandler: (Error e) {
423+
commandError = e;
424+
});
425+
426+
expect(commandError, isA<ToolExit>());
427+
expect(
428+
output,
429+
containsAllInOrder(<Matcher>[
430+
contains('Pre-publish script failed.'),
431+
]),
432+
);
433+
expect(
434+
processRunner.recordedCalls,
435+
containsAllInOrder(<ProcessCall>[
436+
ProcessCall(
437+
getFlutterCommand(mockPlatform),
438+
const <String>[
439+
'pub',
440+
'get',
441+
],
442+
package.directory.path),
443+
ProcessCall(
444+
'dart',
445+
const <String>[
446+
'run',
447+
'tool/pre_publish.dart',
448+
],
449+
package.directory.path),
450+
]));
451+
});
452+
});
453+
372454
test(
373455
'--machine: Log JSON with status:no-publish and correct human message, if there are no packages need to be published. ',
374456
() async {

script/tool/test/publish_command_test.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,91 @@ void main() {
141141
});
142142
});
143143

144+
group('pre-publish script', () {
145+
test('runs if present', () async {
146+
final RepositoryPackage package =
147+
createFakePackage('foo', packagesDir, examples: <String>[]);
148+
package.prePublishScript.createSync(recursive: true);
149+
150+
final List<String> output =
151+
await runCapturingPrint(commandRunner, <String>[
152+
'publish',
153+
'--packages=foo',
154+
]);
155+
156+
expect(
157+
output,
158+
containsAllInOrder(<Matcher>[
159+
contains('Running pre-publish hook tool/pre_publish.dart...'),
160+
]),
161+
);
162+
expect(
163+
processRunner.recordedCalls,
164+
containsAllInOrder(<ProcessCall>[
165+
ProcessCall(
166+
'dart',
167+
const <String>[
168+
'pub',
169+
'get',
170+
],
171+
package.directory.path),
172+
ProcessCall(
173+
'dart',
174+
const <String>[
175+
'run',
176+
'tool/pre_publish.dart',
177+
],
178+
package.directory.path),
179+
]));
180+
});
181+
182+
test('causes command failure if it fails', () async {
183+
final RepositoryPackage package = createFakePackage('foo', packagesDir,
184+
isFlutter: true, examples: <String>[]);
185+
package.prePublishScript.createSync(recursive: true);
186+
187+
processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
188+
FakeProcessInfo(MockProcess(exitCode: 1),
189+
<String>['run']), // run tool/pre_publish.dart
190+
];
191+
192+
Error? commandError;
193+
final List<String> output =
194+
await runCapturingPrint(commandRunner, <String>[
195+
'publish',
196+
'--packages=foo',
197+
], errorHandler: (Error e) {
198+
commandError = e;
199+
});
200+
201+
expect(commandError, isA<ToolExit>());
202+
expect(
203+
output,
204+
containsAllInOrder(<Matcher>[
205+
contains('Pre-publish script failed.'),
206+
]),
207+
);
208+
expect(
209+
processRunner.recordedCalls,
210+
containsAllInOrder(<ProcessCall>[
211+
ProcessCall(
212+
getFlutterCommand(platform),
213+
const <String>[
214+
'pub',
215+
'get',
216+
],
217+
package.directory.path),
218+
ProcessCall(
219+
'dart',
220+
const <String>[
221+
'run',
222+
'tool/pre_publish.dart',
223+
],
224+
package.directory.path),
225+
]));
226+
});
227+
});
228+
144229
group('Publishes package', () {
145230
test('while showing all output from pub publish to the user', () async {
146231
createFakePlugin('plugin1', packagesDir, examples: <String>[]);

0 commit comments

Comments
 (0)