Skip to content

Commit

Permalink
[tool] version-check publish-check commands can check against pub (fl…
Browse files Browse the repository at this point in the history
…utter#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#81444

This PR also fixes some tests.

partially flutter#81444
  • Loading branch information
Chris Yang committed May 11, 2021
1 parent c6065aa commit 0e0c75b
Show file tree
Hide file tree
Showing 8 changed files with 900 additions and 102 deletions.
5 changes: 5 additions & 0 deletions script/tool/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## NEXT

- Add `against-pub` flag for version-check, which allows the command to check version with pub.
- Add `machine` flag for publish-check, which replaces outputs to something parsable by machines.

## 0.1.1

- Update the allowed third-party licenses for flutter/packages.
Expand Down
93 changes: 93 additions & 0 deletions script/tool/lib/src/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:args/command_runner.dart';
import 'package:colorize/colorize.dart';
import 'package:file/file.dart';
import 'package:git/git.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
Expand Down Expand Up @@ -563,6 +564,98 @@ class ProcessRunner {
}
}

/// Finding version of [package] that is published on pub.
class PubVersionFinder {
/// Constructor.
///
/// Note: you should manually close the [httpClient] when done using the finder.
PubVersionFinder({this.pubHost = defaultPubHost, @required this.httpClient});

/// The default pub host to use.
static const String defaultPubHost = 'https://pub.dev';

/// The pub host url, defaults to `https://pub.dev`.
final String pubHost;

/// The http client.
///
/// You should manually close this client when done using this finder.
final http.Client httpClient;

/// Get the package version on pub.
Future<PubVersionFinderResponse> getPackageVersion(
{@required String package}) async {
assert(package != null && package.isNotEmpty);
final Uri pubHostUri = Uri.parse(pubHost);
final Uri url = pubHostUri.replace(path: '/packages/$package.json');
final http.Response response = await httpClient.get(url);

if (response.statusCode == 404) {
return PubVersionFinderResponse(
versions: null,
result: PubVersionFinderResult.noPackageFound,
httpResponse: response);
} else if (response.statusCode != 200) {
return PubVersionFinderResponse(
versions: null,
result: PubVersionFinderResult.fail,
httpResponse: response);
}
final List<Version> versions =
(json.decode(response.body)['versions'] as List<dynamic>)
.map<Version>((final dynamic versionString) =>
Version.parse(versionString as String))
.toList();

return PubVersionFinderResponse(
versions: versions,
result: PubVersionFinderResult.success,
httpResponse: response);
}
}

/// Represents a response for [PubVersionFinder].
class PubVersionFinderResponse {
/// Constructor.
PubVersionFinderResponse({this.versions, this.result, this.httpResponse}) {
if (versions != null && versions.isNotEmpty) {
versions.sort((Version a, Version b) {
// TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize].
// https://github.com/flutter/flutter/issues/82222
return b.compareTo(a);
});
}
}

/// The versions found in [PubVersionFinder].
///
/// This is sorted by largest to smallest, so the first element in the list is the largest version.
/// Might be `null` if the [result] is not [PubVersionFinderResult.success].
final List<Version> versions;

/// The result of the version finder.
final PubVersionFinderResult result;

/// The response object of the http request.
final http.Response httpResponse;
}

/// An enum representing the result of [PubVersionFinder].
enum PubVersionFinderResult {
/// The version finder successfully found a version.
success,

/// The version finder failed to find a valid version.
///
/// This might due to http connection errors or user errors.
fail,

/// The version finder failed to locate the package.
///
/// This indicates the package is new.
noPackageFound,
}

/// Finding diffs based on `baseGitDir` and `baseSha`.
class GitVersionFinder {
/// Constructor
Expand Down
189 changes: 169 additions & 20 deletions script/tool/lib/src/publish_check_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;

import 'package:colorize/colorize.dart';
import 'package:file/file.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:pubspec_parse/pubspec_parse.dart';

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

static const String _allowPrereleaseFlag = 'allow-pre-release';
static const String _machineFlag = 'machine';
static const String _statusNeedsPublish = 'needs-publish';
static const String _statusMessageNoPublish = 'no-publish';
static const String _statusMessageError = 'error';
static const String _statusKey = 'status';
static const String _humanMessageKey = 'humanMessage';

final List<String> _validStatus = <String>[
_statusNeedsPublish,
_statusMessageNoPublish,
_statusMessageError
];

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

/// The custom http client used to query versions on pub.
final http.Client httpClient;

final PubVersionFinder _pubVersionFinder;

// The output JSON when the _machineFlag is on.
final Map<String, dynamic> _machineOutput = <String, dynamic>{};

final List<String> _humanMessages = <String>[];

@override
Future<void> run() async {
final ZoneSpecification logSwitchSpecification = ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String message) {
final bool logMachineMessage = argResults[_machineFlag] as bool;
if (logMachineMessage && message != _prettyJson(_machineOutput)) {
_humanMessages.add(message);
} else {
parent.print(zone, message);
}
});

await runZoned(_runCommand, zoneSpecification: logSwitchSpecification);
}

Future<void> _runCommand() async {
final List<Directory> failedPackages = <Directory>[];

String status = _statusMessageNoPublish;
await for (final Directory plugin in getPlugins()) {
if (!(await _passesPublishCheck(plugin))) {
failedPackages.add(plugin);
final _PublishCheckResult result = await _passesPublishCheck(plugin);
switch (result) {
case _PublishCheckResult._notPublished:
if (failedPackages.isEmpty) {
status = _statusNeedsPublish;
}
break;
case _PublishCheckResult._published:
break;
case _PublishCheckResult._error:
failedPackages.add(plugin);
status = _statusMessageError;
break;
}
}
_pubVersionFinder.httpClient.close();

if (failedPackages.isNotEmpty) {
final String error =
'FAIL: The following ${failedPackages.length} package(s) failed the '
'The following ${failedPackages.length} package(s) failed the '
'publishing check:';
final String joinedFailedPackages = failedPackages.join('\n');
_printImportantStatusMessage('$error\n$joinedFailedPackages',
isError: true);
} else {
_printImportantStatusMessage('All packages passed publish check!',
isError: false);
}

final Colorize colorizedError = Colorize('$error\n$joinedFailedPackages')
..red();
print(colorizedError);
throw ToolExit(1);
if (argResults[_machineFlag] as bool) {
_setStatus(status);
_machineOutput[_humanMessageKey] = _humanMessages;
print(_prettyJson(_machineOutput));
}

final Colorize passedMessage =
Colorize('All packages passed publish check!')..green();
print(passedMessage);
if (failedPackages.isNotEmpty) {
throw ToolExit(1);
}
}

Pubspec _tryParsePubspec(Directory package) {
Expand Down Expand Up @@ -89,17 +159,23 @@ class PublishCheckCommand extends PluginCommand {
final Completer<void> stdOutCompleter = Completer<void>();
process.stdout.listen(
(List<int> event) {
io.stdout.add(event);
outputBuffer.write(String.fromCharCodes(event));
final String output = String.fromCharCodes(event);
if (output.isNotEmpty) {
print(output);
outputBuffer.write(output);
}
},
onDone: () => stdOutCompleter.complete(),
);

final Completer<void> stdInCompleter = Completer<void>();
process.stderr.listen(
(List<int> event) {
io.stderr.add(event);
outputBuffer.write(String.fromCharCodes(event));
final String output = String.fromCharCodes(event);
if (output.isNotEmpty) {
_printImportantStatusMessage(output, isError: true);
outputBuffer.write(output);
}
},
onDone: () => stdInCompleter.complete(),
);
Expand All @@ -121,24 +197,97 @@ class PublishCheckCommand extends PluginCommand {
'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.');
}

Future<bool> _passesPublishCheck(Directory package) async {
Future<_PublishCheckResult> _passesPublishCheck(Directory package) async {
final String packageName = package.basename;
print('Checking that $packageName can be published.');

final Pubspec pubspec = _tryParsePubspec(package);
if (pubspec == null) {
return false;
print('no pubspec');
return _PublishCheckResult._error;
} else if (pubspec.publishTo == 'none') {
print('Package $packageName is marked as unpublishable. Skipping.');
return true;
return _PublishCheckResult._published;
}

final Version version = pubspec.version;
final _PublishCheckResult alreadyPublishedResult =
await _checkIfAlreadyPublished(
packageName: packageName, version: version);
if (alreadyPublishedResult == _PublishCheckResult._published) {
print(
'Package $packageName version: $version has already be published on pub.');
return alreadyPublishedResult;
} else if (alreadyPublishedResult == _PublishCheckResult._error) {
print('Check pub version failed $packageName');
return _PublishCheckResult._error;
}

if (await _hasValidPublishCheckRun(package)) {
print('Package $packageName is able to be published.');
return true;
return _PublishCheckResult._notPublished;
} else {
print('Unable to publish $packageName');
return false;
return _PublishCheckResult._error;
}
}

// Check if `packageName` already has `version` published on pub.
Future<_PublishCheckResult> _checkIfAlreadyPublished(
{String packageName, Version version}) async {
final PubVersionFinderResponse pubVersionFinderResponse =
await _pubVersionFinder.getPackageVersion(package: packageName);
_PublishCheckResult result;
switch (pubVersionFinderResponse.result) {
case PubVersionFinderResult.success:
result = pubVersionFinderResponse.versions.contains(version)
? _PublishCheckResult._published
: _PublishCheckResult._notPublished;
break;
case PubVersionFinderResult.fail:
print('''
Error fetching version on pub for $packageName.
HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode}
HTTP response: ${pubVersionFinderResponse.httpResponse.body}
''');
result = _PublishCheckResult._error;
break;
case PubVersionFinderResult.noPackageFound:
result = _PublishCheckResult._notPublished;
break;
}
return result;
}

void _setStatus(String status) {
assert(_validStatus.contains(status));
_machineOutput[_statusKey] = status;
}

String _prettyJson(Map<String, dynamic> map) {
return const JsonEncoder.withIndent(' ').convert(_machineOutput);
}

void _printImportantStatusMessage(String message, {@required bool isError}) {
final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message';
if (argResults[_machineFlag] as bool) {
print(statusMessage);
} else {
final Colorize colorizedMessage = Colorize(statusMessage);
if (isError) {
colorizedMessage.red();
} else {
colorizedMessage.green();
}
print(colorizedMessage);
}
}
}

enum _PublishCheckResult {
_notPublished,

_published,

_error,
}

0 comments on commit 0e0c75b

Please sign in to comment.