33// found in the LICENSE file.
44
55import 'dart:async' ;
6+ import 'dart:convert' ;
67import 'dart:io' as io;
78
89import 'package:colorize/colorize.dart' ;
910import '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' ;
1014import 'package:pubspec_parse/pubspec_parse.dart' ;
1115
1216import '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