mirror of
https://github.com/flutter/packages.git
synced 2025-06-29 06:06:59 +08:00
[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
This commit is contained in:
@ -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';
|
||||
@ -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
|
||||
|
@ -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';
|
||||
@ -18,7 +22,10 @@ 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'
|
||||
@ -26,9 +33,29 @@ class PublishCheckCommand extends PluginCommand {
|
||||
'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';
|
||||
@ -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');
|
||||
|
||||
final Colorize colorizedError = Colorize('$error\n$joinedFailedPackages')
|
||||
..red();
|
||||
print(colorizedError);
|
||||
throw ToolExit(1);
|
||||
_printImportantStatusMessage('$error\n$joinedFailedPackages',
|
||||
isError: true);
|
||||
} else {
|
||||
_printImportantStatusMessage('All packages passed publish check!',
|
||||
isError: false);
|
||||
}
|
||||
|
||||
final Colorize passedMessage =
|
||||
Colorize('All packages passed publish check!')..green();
|
||||
print(passedMessage);
|
||||
if (argResults[_machineFlag] as bool) {
|
||||
_setStatus(status);
|
||||
_machineOutput[_humanMessageKey] = _humanMessages;
|
||||
print(_prettyJson(_machineOutput));
|
||||
}
|
||||
|
||||
if (failedPackages.isNotEmpty) {
|
||||
throw ToolExit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Pubspec _tryParsePubspec(Directory package) {
|
||||
@ -89,8 +159,11 @@ 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(),
|
||||
);
|
||||
@ -98,8 +171,11 @@ class PublishCheckCommand extends PluginCommand {
|
||||
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(),
|
||||
);
|
||||
@ -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,
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:git/git.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';
|
||||
@ -74,8 +75,21 @@ class VersionCheckCommand extends PluginCommand {
|
||||
FileSystem fileSystem, {
|
||||
ProcessRunner processRunner = const ProcessRunner(),
|
||||
GitDir gitDir,
|
||||
}) : super(packagesDir, fileSystem,
|
||||
processRunner: processRunner, gitDir: gitDir);
|
||||
this.httpClient,
|
||||
}) : _pubVersionFinder =
|
||||
PubVersionFinder(httpClient: httpClient ?? http.Client()),
|
||||
super(packagesDir, fileSystem,
|
||||
processRunner: processRunner, gitDir: gitDir) {
|
||||
argParser.addFlag(
|
||||
_againstPubFlag,
|
||||
help: 'Whether the version check should run against the version on pub.\n'
|
||||
'Defaults to false, which means the version check only run against the previous version in code.',
|
||||
defaultsTo: false,
|
||||
negatable: true,
|
||||
);
|
||||
}
|
||||
|
||||
static const String _againstPubFlag = 'against-pub';
|
||||
|
||||
@override
|
||||
final String name = 'version-check';
|
||||
@ -86,6 +100,11 @@ class VersionCheckCommand extends PluginCommand {
|
||||
'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n'
|
||||
'This command requires "pub" and "flutter" to be in your path.';
|
||||
|
||||
/// The http client used to query pub server.
|
||||
final http.Client httpClient;
|
||||
|
||||
final PubVersionFinder _pubVersionFinder;
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
|
||||
@ -115,29 +134,61 @@ class VersionCheckCommand extends PluginCommand {
|
||||
'intentionally has no version should be marked '
|
||||
'"publish_to: none".');
|
||||
}
|
||||
final Version masterVersion =
|
||||
await gitVersionFinder.getPackageVersion(pubspecPath);
|
||||
if (masterVersion == null) {
|
||||
print('${indentation}Unable to find pubspec in master. '
|
||||
'Safe to ignore if the project is new.');
|
||||
Version sourceVersion;
|
||||
if (argResults[_againstPubFlag] as bool) {
|
||||
final String packageName = pubspecFile.parent.basename;
|
||||
final PubVersionFinderResponse pubVersionFinderResponse =
|
||||
await _pubVersionFinder.getPackageVersion(package: packageName);
|
||||
switch (pubVersionFinderResponse.result) {
|
||||
case PubVersionFinderResult.success:
|
||||
sourceVersion = pubVersionFinderResponse.versions.first;
|
||||
print(
|
||||
'$indentation$packageName: Current largest version on pub: $sourceVersion');
|
||||
break;
|
||||
case PubVersionFinderResult.fail:
|
||||
printErrorAndExit(errorMessage: '''
|
||||
${indentation}Error fetching version on pub for $packageName.
|
||||
${indentation}HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode}
|
||||
${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body}
|
||||
''');
|
||||
break;
|
||||
case PubVersionFinderResult.noPackageFound:
|
||||
sourceVersion = null;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sourceVersion = await gitVersionFinder.getPackageVersion(pubspecPath);
|
||||
}
|
||||
if (sourceVersion == null) {
|
||||
String safeToIgnoreMessage;
|
||||
if (argResults[_againstPubFlag] as bool) {
|
||||
safeToIgnoreMessage =
|
||||
'${indentation}Unable to find package on pub server.';
|
||||
} else {
|
||||
safeToIgnoreMessage =
|
||||
'${indentation}Unable to find pubspec in master.';
|
||||
}
|
||||
print('$safeToIgnoreMessage Safe to ignore if the project is new.');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (masterVersion == headVersion) {
|
||||
if (sourceVersion == headVersion) {
|
||||
print('${indentation}No version change.');
|
||||
continue;
|
||||
}
|
||||
|
||||
final Map<Version, NextVersionType> allowedNextVersions =
|
||||
getAllowedNextVersions(masterVersion, headVersion);
|
||||
getAllowedNextVersions(sourceVersion, headVersion);
|
||||
|
||||
if (!allowedNextVersions.containsKey(headVersion)) {
|
||||
final String source =
|
||||
(argResults[_againstPubFlag] as bool) ? 'pub' : 'master';
|
||||
final String error = '${indentation}Incorrectly updated version.\n'
|
||||
'${indentation}HEAD: $headVersion, master: $masterVersion.\n'
|
||||
'${indentation}HEAD: $headVersion, $source: $sourceVersion.\n'
|
||||
'${indentation}Allowed versions: $allowedNextVersions';
|
||||
printErrorAndExit(errorMessage: error);
|
||||
} else {
|
||||
print('$indentation$headVersion -> $masterVersion');
|
||||
print('$indentation$headVersion -> $sourceVersion');
|
||||
}
|
||||
|
||||
final bool isPlatformInterface =
|
||||
@ -153,6 +204,7 @@ class VersionCheckCommand extends PluginCommand {
|
||||
await for (final Directory plugin in getPlugins()) {
|
||||
await _checkVersionsMatch(plugin);
|
||||
}
|
||||
_pubVersionFinder.httpClient.close();
|
||||
|
||||
print('No version check errors found!');
|
||||
}
|
||||
@ -224,7 +276,7 @@ The first version listed in CHANGELOG.md is $fromChangeLog.
|
||||
printErrorAndExit(errorMessage: '''
|
||||
When bumping the version for release, the NEXT section should be incorporated
|
||||
into the new version's release notes.
|
||||
''');
|
||||
''');
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user