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

Commit 0e0c75b

Browse files
author
Chris Yang
authored
[tool] version-check publish-check commands can check against pub (#3840)
Add a PubVersionFinder class to easily fetch the version from pub. Add an against-pub flag to check-version command, which allows it to check the version against pub server Make the 'publish-check' command to check against pub to determine if the specific versions of packages need to be published. Add a log-status flag, which allows the publish-check command to log the final status of the result. This helps other ci tools to easily grab the results and use it to determine what to do next. See option 3 in flutter/flutter#81444 This PR also fixes some tests. partially flutter/flutter#81444
1 parent c6065aa commit 0e0c75b

8 files changed

+900
-102
lines changed

script/tool/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## NEXT
2+
3+
- Add `against-pub` flag for version-check, which allows the command to check version with pub.
4+
- Add `machine` flag for publish-check, which replaces outputs to something parsable by machines.
5+
16
## 0.1.1
27

38
- Update the allowed third-party licenses for flutter/packages.

script/tool/lib/src/common.dart

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:args/command_runner.dart';
1111
import 'package:colorize/colorize.dart';
1212
import 'package:file/file.dart';
1313
import 'package:git/git.dart';
14+
import 'package:http/http.dart' as http;
1415
import 'package:meta/meta.dart';
1516
import 'package:path/path.dart' as p;
1617
import 'package:pub_semver/pub_semver.dart';
@@ -563,6 +564,98 @@ class ProcessRunner {
563564
}
564565
}
565566

567+
/// Finding version of [package] that is published on pub.
568+
class PubVersionFinder {
569+
/// Constructor.
570+
///
571+
/// Note: you should manually close the [httpClient] when done using the finder.
572+
PubVersionFinder({this.pubHost = defaultPubHost, @required this.httpClient});
573+
574+
/// The default pub host to use.
575+
static const String defaultPubHost = 'https://pub.dev';
576+
577+
/// The pub host url, defaults to `https://pub.dev`.
578+
final String pubHost;
579+
580+
/// The http client.
581+
///
582+
/// You should manually close this client when done using this finder.
583+
final http.Client httpClient;
584+
585+
/// Get the package version on pub.
586+
Future<PubVersionFinderResponse> getPackageVersion(
587+
{@required String package}) async {
588+
assert(package != null && package.isNotEmpty);
589+
final Uri pubHostUri = Uri.parse(pubHost);
590+
final Uri url = pubHostUri.replace(path: '/packages/$package.json');
591+
final http.Response response = await httpClient.get(url);
592+
593+
if (response.statusCode == 404) {
594+
return PubVersionFinderResponse(
595+
versions: null,
596+
result: PubVersionFinderResult.noPackageFound,
597+
httpResponse: response);
598+
} else if (response.statusCode != 200) {
599+
return PubVersionFinderResponse(
600+
versions: null,
601+
result: PubVersionFinderResult.fail,
602+
httpResponse: response);
603+
}
604+
final List<Version> versions =
605+
(json.decode(response.body)['versions'] as List<dynamic>)
606+
.map<Version>((final dynamic versionString) =>
607+
Version.parse(versionString as String))
608+
.toList();
609+
610+
return PubVersionFinderResponse(
611+
versions: versions,
612+
result: PubVersionFinderResult.success,
613+
httpResponse: response);
614+
}
615+
}
616+
617+
/// Represents a response for [PubVersionFinder].
618+
class PubVersionFinderResponse {
619+
/// Constructor.
620+
PubVersionFinderResponse({this.versions, this.result, this.httpResponse}) {
621+
if (versions != null && versions.isNotEmpty) {
622+
versions.sort((Version a, Version b) {
623+
// TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize].
624+
// https://github.com/flutter/flutter/issues/82222
625+
return b.compareTo(a);
626+
});
627+
}
628+
}
629+
630+
/// The versions found in [PubVersionFinder].
631+
///
632+
/// This is sorted by largest to smallest, so the first element in the list is the largest version.
633+
/// Might be `null` if the [result] is not [PubVersionFinderResult.success].
634+
final List<Version> versions;
635+
636+
/// The result of the version finder.
637+
final PubVersionFinderResult result;
638+
639+
/// The response object of the http request.
640+
final http.Response httpResponse;
641+
}
642+
643+
/// An enum representing the result of [PubVersionFinder].
644+
enum PubVersionFinderResult {
645+
/// The version finder successfully found a version.
646+
success,
647+
648+
/// The version finder failed to find a valid version.
649+
///
650+
/// This might due to http connection errors or user errors.
651+
fail,
652+
653+
/// The version finder failed to locate the package.
654+
///
655+
/// This indicates the package is new.
656+
noPackageFound,
657+
}
658+
566659
/// Finding diffs based on `baseGitDir` and `baseSha`.
567660
class GitVersionFinder {
568661
/// Constructor

script/tool/lib/src/publish_check_command.dart

Lines changed: 169 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67
import 'dart:io' as io;
78

89
import 'package:colorize/colorize.dart';
910
import 'package:file/file.dart';
11+
import 'package:http/http.dart' as http;
12+
import 'package:meta/meta.dart';
13+
import 'package:pub_semver/pub_semver.dart';
1014
import 'package:pubspec_parse/pubspec_parse.dart';
1115

1216
import 'common.dart';
@@ -18,17 +22,40 @@ class PublishCheckCommand extends PluginCommand {
1822
Directory packagesDir,
1923
FileSystem fileSystem, {
2024
ProcessRunner processRunner = const ProcessRunner(),
21-
}) : super(packagesDir, fileSystem, processRunner: processRunner) {
25+
this.httpClient,
26+
}) : _pubVersionFinder =
27+
PubVersionFinder(httpClient: httpClient ?? http.Client()),
28+
super(packagesDir, fileSystem, processRunner: processRunner) {
2229
argParser.addFlag(
2330
_allowPrereleaseFlag,
2431
help: 'Allows the pre-release SDK warning to pass.\n'
2532
'When enabled, a pub warning, which asks to publish the package as a pre-release version when '
2633
'the SDK constraint is a pre-release version, is ignored.',
2734
defaultsTo: false,
2835
);
36+
argParser.addFlag(_machineFlag,
37+
help: 'Switch outputs to a machine readable JSON. \n'
38+
'The JSON contains a "status" field indicating the final status of the command, the possible values are:\n'
39+
' $_statusNeedsPublish: There is at least one package need to be published. They also passed all publish checks.\n'
40+
' $_statusMessageNoPublish: There are no packages needs to be published. Either no pubspec change detected or all versions have already been published.\n'
41+
' $_statusMessageError: Some error has occurred.',
42+
defaultsTo: false,
43+
negatable: true);
2944
}
3045

3146
static const String _allowPrereleaseFlag = 'allow-pre-release';
47+
static const String _machineFlag = 'machine';
48+
static const String _statusNeedsPublish = 'needs-publish';
49+
static const String _statusMessageNoPublish = 'no-publish';
50+
static const String _statusMessageError = 'error';
51+
static const String _statusKey = 'status';
52+
static const String _humanMessageKey = 'humanMessage';
53+
54+
final List<String> _validStatus = <String>[
55+
_statusNeedsPublish,
56+
_statusMessageNoPublish,
57+
_statusMessageError
58+
];
3259

3360
@override
3461
final String name = 'publish-check';
@@ -37,31 +64,74 @@ class PublishCheckCommand extends PluginCommand {
3764
final String description =
3865
'Checks to make sure that a plugin *could* be published.';
3966

67+
/// The custom http client used to query versions on pub.
68+
final http.Client httpClient;
69+
70+
final PubVersionFinder _pubVersionFinder;
71+
72+
// The output JSON when the _machineFlag is on.
73+
final Map<String, dynamic> _machineOutput = <String, dynamic>{};
74+
75+
final List<String> _humanMessages = <String>[];
76+
4077
@override
4178
Future<void> run() async {
79+
final ZoneSpecification logSwitchSpecification = ZoneSpecification(
80+
print: (Zone self, ZoneDelegate parent, Zone zone, String message) {
81+
final bool logMachineMessage = argResults[_machineFlag] as bool;
82+
if (logMachineMessage && message != _prettyJson(_machineOutput)) {
83+
_humanMessages.add(message);
84+
} else {
85+
parent.print(zone, message);
86+
}
87+
});
88+
89+
await runZoned(_runCommand, zoneSpecification: logSwitchSpecification);
90+
}
91+
92+
Future<void> _runCommand() async {
4293
final List<Directory> failedPackages = <Directory>[];
4394

95+
String status = _statusMessageNoPublish;
4496
await for (final Directory plugin in getPlugins()) {
45-
if (!(await _passesPublishCheck(plugin))) {
46-
failedPackages.add(plugin);
97+
final _PublishCheckResult result = await _passesPublishCheck(plugin);
98+
switch (result) {
99+
case _PublishCheckResult._notPublished:
100+
if (failedPackages.isEmpty) {
101+
status = _statusNeedsPublish;
102+
}
103+
break;
104+
case _PublishCheckResult._published:
105+
break;
106+
case _PublishCheckResult._error:
107+
failedPackages.add(plugin);
108+
status = _statusMessageError;
109+
break;
47110
}
48111
}
112+
_pubVersionFinder.httpClient.close();
49113

50114
if (failedPackages.isNotEmpty) {
51115
final String error =
52-
'FAIL: The following ${failedPackages.length} package(s) failed the '
116+
'The following ${failedPackages.length} package(s) failed the '
53117
'publishing check:';
54118
final String joinedFailedPackages = failedPackages.join('\n');
119+
_printImportantStatusMessage('$error\n$joinedFailedPackages',
120+
isError: true);
121+
} else {
122+
_printImportantStatusMessage('All packages passed publish check!',
123+
isError: false);
124+
}
55125

56-
final Colorize colorizedError = Colorize('$error\n$joinedFailedPackages')
57-
..red();
58-
print(colorizedError);
59-
throw ToolExit(1);
126+
if (argResults[_machineFlag] as bool) {
127+
_setStatus(status);
128+
_machineOutput[_humanMessageKey] = _humanMessages;
129+
print(_prettyJson(_machineOutput));
60130
}
61131

62-
final Colorize passedMessage =
63-
Colorize('All packages passed publish check!')..green();
64-
print(passedMessage);
132+
if (failedPackages.isNotEmpty) {
133+
throw ToolExit(1);
134+
}
65135
}
66136

67137
Pubspec _tryParsePubspec(Directory package) {
@@ -89,17 +159,23 @@ class PublishCheckCommand extends PluginCommand {
89159
final Completer<void> stdOutCompleter = Completer<void>();
90160
process.stdout.listen(
91161
(List<int> event) {
92-
io.stdout.add(event);
93-
outputBuffer.write(String.fromCharCodes(event));
162+
final String output = String.fromCharCodes(event);
163+
if (output.isNotEmpty) {
164+
print(output);
165+
outputBuffer.write(output);
166+
}
94167
},
95168
onDone: () => stdOutCompleter.complete(),
96169
);
97170

98171
final Completer<void> stdInCompleter = Completer<void>();
99172
process.stderr.listen(
100173
(List<int> event) {
101-
io.stderr.add(event);
102-
outputBuffer.write(String.fromCharCodes(event));
174+
final String output = String.fromCharCodes(event);
175+
if (output.isNotEmpty) {
176+
_printImportantStatusMessage(output, isError: true);
177+
outputBuffer.write(output);
178+
}
103179
},
104180
onDone: () => stdInCompleter.complete(),
105181
);
@@ -121,24 +197,97 @@ class PublishCheckCommand extends PluginCommand {
121197
'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.');
122198
}
123199

124-
Future<bool> _passesPublishCheck(Directory package) async {
200+
Future<_PublishCheckResult> _passesPublishCheck(Directory package) async {
125201
final String packageName = package.basename;
126202
print('Checking that $packageName can be published.');
127203

128204
final Pubspec pubspec = _tryParsePubspec(package);
129205
if (pubspec == null) {
130-
return false;
206+
print('no pubspec');
207+
return _PublishCheckResult._error;
131208
} else if (pubspec.publishTo == 'none') {
132209
print('Package $packageName is marked as unpublishable. Skipping.');
133-
return true;
210+
return _PublishCheckResult._published;
211+
}
212+
213+
final Version version = pubspec.version;
214+
final _PublishCheckResult alreadyPublishedResult =
215+
await _checkIfAlreadyPublished(
216+
packageName: packageName, version: version);
217+
if (alreadyPublishedResult == _PublishCheckResult._published) {
218+
print(
219+
'Package $packageName version: $version has already be published on pub.');
220+
return alreadyPublishedResult;
221+
} else if (alreadyPublishedResult == _PublishCheckResult._error) {
222+
print('Check pub version failed $packageName');
223+
return _PublishCheckResult._error;
134224
}
135225

136226
if (await _hasValidPublishCheckRun(package)) {
137227
print('Package $packageName is able to be published.');
138-
return true;
228+
return _PublishCheckResult._notPublished;
139229
} else {
140230
print('Unable to publish $packageName');
141-
return false;
231+
return _PublishCheckResult._error;
232+
}
233+
}
234+
235+
// Check if `packageName` already has `version` published on pub.
236+
Future<_PublishCheckResult> _checkIfAlreadyPublished(
237+
{String packageName, Version version}) async {
238+
final PubVersionFinderResponse pubVersionFinderResponse =
239+
await _pubVersionFinder.getPackageVersion(package: packageName);
240+
_PublishCheckResult result;
241+
switch (pubVersionFinderResponse.result) {
242+
case PubVersionFinderResult.success:
243+
result = pubVersionFinderResponse.versions.contains(version)
244+
? _PublishCheckResult._published
245+
: _PublishCheckResult._notPublished;
246+
break;
247+
case PubVersionFinderResult.fail:
248+
print('''
249+
Error fetching version on pub for $packageName.
250+
HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode}
251+
HTTP response: ${pubVersionFinderResponse.httpResponse.body}
252+
''');
253+
result = _PublishCheckResult._error;
254+
break;
255+
case PubVersionFinderResult.noPackageFound:
256+
result = _PublishCheckResult._notPublished;
257+
break;
142258
}
259+
return result;
260+
}
261+
262+
void _setStatus(String status) {
263+
assert(_validStatus.contains(status));
264+
_machineOutput[_statusKey] = status;
265+
}
266+
267+
String _prettyJson(Map<String, dynamic> map) {
268+
return const JsonEncoder.withIndent(' ').convert(_machineOutput);
143269
}
270+
271+
void _printImportantStatusMessage(String message, {@required bool isError}) {
272+
final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message';
273+
if (argResults[_machineFlag] as bool) {
274+
print(statusMessage);
275+
} else {
276+
final Colorize colorizedMessage = Colorize(statusMessage);
277+
if (isError) {
278+
colorizedMessage.red();
279+
} else {
280+
colorizedMessage.green();
281+
}
282+
print(colorizedMessage);
283+
}
284+
}
285+
}
286+
287+
enum _PublishCheckResult {
288+
_notPublished,
289+
290+
_published,
291+
292+
_error,
144293
}

0 commit comments

Comments
 (0)