mirror of
https://github.com/flutter/packages.git
synced 2025-06-17 02:48:43 +08:00
[tool] add all
and dry-run
flags to publish-plugin command (#3776)
This commit is contained in:
@ -4,12 +4,14 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:git/git.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:pubspec_parse/pubspec_parse.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import 'common.dart';
|
||||
@ -32,10 +34,12 @@ class PublishPluginCommand extends PluginCommand {
|
||||
FileSystem fileSystem, {
|
||||
ProcessRunner processRunner = const ProcessRunner(),
|
||||
Print print = print,
|
||||
Stdin stdinput,
|
||||
io.Stdin stdinput,
|
||||
GitDir gitDir,
|
||||
}) : _print = print,
|
||||
_stdin = stdinput ?? stdin,
|
||||
super(packagesDir, fileSystem, processRunner: processRunner) {
|
||||
_stdin = stdinput ?? io.stdin,
|
||||
super(packagesDir, fileSystem,
|
||||
processRunner: processRunner, gitDir: gitDir) {
|
||||
argParser.addOption(
|
||||
_packageOption,
|
||||
help: 'The package to publish.'
|
||||
@ -64,6 +68,22 @@ class PublishPluginCommand extends PluginCommand {
|
||||
// Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks.
|
||||
defaultsTo: 'upstream',
|
||||
);
|
||||
argParser.addFlag(
|
||||
_allChangedFlag,
|
||||
help:
|
||||
'Release all plugins that contains pubspec changes at the current commit compares to the base-sha.\n'
|
||||
'The $_packageOption option is ignored if this is on.',
|
||||
defaultsTo: false,
|
||||
);
|
||||
argParser.addFlag(
|
||||
_dryRunFlag,
|
||||
help:
|
||||
'Skips the real `pub publish` and `git tag` commands and assumes both commands are successful.\n'
|
||||
'This does not run `pub publish --dry-run`.\n'
|
||||
'If you want to run the command with `pub publish --dry-run`, use `pub-publish-flags=--dry-run`',
|
||||
defaultsTo: false,
|
||||
negatable: true,
|
||||
);
|
||||
}
|
||||
|
||||
static const String _packageOption = 'package';
|
||||
@ -71,6 +91,8 @@ class PublishPluginCommand extends PluginCommand {
|
||||
static const String _pushTagsOption = 'push-tags';
|
||||
static const String _pubFlagsOption = 'pub-publish-flags';
|
||||
static const String _remoteOption = 'remote';
|
||||
static const String _allChangedFlag = 'all-changed';
|
||||
static const String _dryRunFlag = 'dry-run';
|
||||
|
||||
// Version tags should follow <package-name>-v<semantic-version>. For example,
|
||||
// `flutter_plugin_tools-v0.0.24`.
|
||||
@ -84,14 +106,14 @@ class PublishPluginCommand extends PluginCommand {
|
||||
'Attempts to publish the given plugin and tag its release on GitHub.';
|
||||
|
||||
final Print _print;
|
||||
final Stdin _stdin;
|
||||
// The directory of the actual package that we are publishing.
|
||||
final io.Stdin _stdin;
|
||||
StreamSubscription<String> _stdinSubscription;
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final String package = argResults[_packageOption] as String;
|
||||
if (package == null) {
|
||||
final bool publishAllChanged = argResults[_allChangedFlag] as bool;
|
||||
if (package == null && !publishAllChanged) {
|
||||
_print(
|
||||
'Must specify a package to publish. See `plugin_tools help publish-plugin`.');
|
||||
throw ToolExit(1);
|
||||
@ -102,6 +124,8 @@ class PublishPluginCommand extends PluginCommand {
|
||||
_print('$packagesDir is not a valid Git repository.');
|
||||
throw ToolExit(1);
|
||||
}
|
||||
final GitDir baseGitDir =
|
||||
await GitDir.fromExisting(packagesDir.path, allowSubdirectory: true);
|
||||
|
||||
final bool shouldPushTag = argResults[_pushTagsOption] == true;
|
||||
final String remote = argResults[_remoteOption] as String;
|
||||
@ -110,50 +134,229 @@ class PublishPluginCommand extends PluginCommand {
|
||||
remoteUrl = await _verifyRemote(remote);
|
||||
}
|
||||
_print('Local repo is ready!');
|
||||
|
||||
final Directory packageDir = _getPackageDir(package);
|
||||
await _publishPlugin(packageDir: packageDir);
|
||||
if (argResults[_tagReleaseOption] as bool) {
|
||||
await _tagRelease(
|
||||
packageDir: packageDir,
|
||||
remote: remote,
|
||||
remoteUrl: remoteUrl,
|
||||
shouldPushTag: shouldPushTag);
|
||||
if (argResults[_dryRunFlag] as bool) {
|
||||
_print('=============== DRY RUN ===============');
|
||||
}
|
||||
await _finishSuccesfully();
|
||||
|
||||
bool successful;
|
||||
if (publishAllChanged) {
|
||||
successful = await _publishAllChangedPackages(
|
||||
remote: remote,
|
||||
remoteUrl: remoteUrl,
|
||||
shouldPushTag: shouldPushTag,
|
||||
baseGitDir: baseGitDir,
|
||||
);
|
||||
} else {
|
||||
successful = await _publishAndTagPackage(
|
||||
packageDir: _getPackageDir(package),
|
||||
remote: remote,
|
||||
remoteUrl: remoteUrl,
|
||||
shouldPushTag: shouldPushTag,
|
||||
);
|
||||
}
|
||||
await _finish(successful);
|
||||
}
|
||||
|
||||
Future<void> _publishPlugin({@required Directory packageDir}) async {
|
||||
await _checkGitStatus(packageDir);
|
||||
await _publish(packageDir);
|
||||
Future<bool> _publishAllChangedPackages({
|
||||
String remote,
|
||||
String remoteUrl,
|
||||
bool shouldPushTag,
|
||||
GitDir baseGitDir,
|
||||
}) async {
|
||||
final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
|
||||
final List<String> changedPubspecs =
|
||||
await gitVersionFinder.getChangedPubSpecs();
|
||||
if (changedPubspecs.isEmpty) {
|
||||
_print('No version updates in this commit.');
|
||||
return true;
|
||||
}
|
||||
_print('Getting existing tags...');
|
||||
final io.ProcessResult existingTagsResult =
|
||||
await baseGitDir.runCommand(<String>['tag', '--sort=-committerdate']);
|
||||
final List<String> existingTags = (existingTagsResult.stdout as String)
|
||||
.split('\n')
|
||||
..removeWhere((String element) => element.isEmpty);
|
||||
|
||||
final List<String> packagesReleased = <String>[];
|
||||
final List<String> packagesFailed = <String>[];
|
||||
|
||||
for (final String pubspecPath in changedPubspecs) {
|
||||
final File pubspecFile =
|
||||
fileSystem.directory(baseGitDir.path).childFile(pubspecPath);
|
||||
final _CheckNeedsReleaseResult result = await _checkNeedsRelease(
|
||||
pubspecFile: pubspecFile,
|
||||
gitVersionFinder: gitVersionFinder,
|
||||
existingTags: existingTags,
|
||||
);
|
||||
switch (result) {
|
||||
case _CheckNeedsReleaseResult.release:
|
||||
break;
|
||||
case _CheckNeedsReleaseResult.noRelease:
|
||||
continue;
|
||||
case _CheckNeedsReleaseResult.failure:
|
||||
packagesFailed.add(pubspecFile.parent.basename);
|
||||
continue;
|
||||
}
|
||||
_print('\n');
|
||||
if (await _publishAndTagPackage(
|
||||
packageDir: pubspecFile.parent,
|
||||
remote: remote,
|
||||
remoteUrl: remoteUrl,
|
||||
shouldPushTag: shouldPushTag,
|
||||
)) {
|
||||
packagesReleased.add(pubspecFile.parent.basename);
|
||||
} else {
|
||||
packagesFailed.add(pubspecFile.parent.basename);
|
||||
}
|
||||
_print('\n');
|
||||
}
|
||||
if (packagesReleased.isNotEmpty) {
|
||||
_print('Packages released: ${packagesReleased.join(', ')}');
|
||||
}
|
||||
if (packagesFailed.isNotEmpty) {
|
||||
_print(
|
||||
'Failed to release the following packages: ${packagesFailed.join(', ')}, see above for details.');
|
||||
}
|
||||
return packagesFailed.isEmpty;
|
||||
}
|
||||
|
||||
// Publish the package to pub with `pub publish`.
|
||||
// If `_tagReleaseOption` is on, git tag the release.
|
||||
// If `shouldPushTag` is `true`, the tag will be pushed to `remote`.
|
||||
// Returns `true` if publishing and tag are successful.
|
||||
Future<bool> _publishAndTagPackage({
|
||||
@required Directory packageDir,
|
||||
@required String remote,
|
||||
@required String remoteUrl,
|
||||
@required bool shouldPushTag,
|
||||
}) async {
|
||||
if (!await _publishPlugin(packageDir: packageDir)) {
|
||||
return false;
|
||||
}
|
||||
if (argResults[_tagReleaseOption] as bool) {
|
||||
if (!await _tagRelease(
|
||||
packageDir: packageDir,
|
||||
remote: remote,
|
||||
remoteUrl: remoteUrl,
|
||||
shouldPushTag: shouldPushTag,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_print('Released [${packageDir.basename}] successfully.');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns a [_CheckNeedsReleaseResult] that indicates the result.
|
||||
Future<_CheckNeedsReleaseResult> _checkNeedsRelease({
|
||||
@required File pubspecFile,
|
||||
@required GitVersionFinder gitVersionFinder,
|
||||
@required List<String> existingTags,
|
||||
}) async {
|
||||
if (!pubspecFile.existsSync()) {
|
||||
_print('''
|
||||
The file at The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}.
|
||||
Safe to ignore if the package is deleted in this commit.
|
||||
''');
|
||||
return _CheckNeedsReleaseResult.noRelease;
|
||||
}
|
||||
|
||||
final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
|
||||
if (pubspec.publishTo == 'none') {
|
||||
return _CheckNeedsReleaseResult.noRelease;
|
||||
}
|
||||
|
||||
if (pubspec.version == null) {
|
||||
_print('No version found. A package that intentionally has no version should be marked "publish_to: none"');
|
||||
return _CheckNeedsReleaseResult.failure;
|
||||
}
|
||||
|
||||
if (pubspec.name == null) {
|
||||
_print('Fatal: Package name is null.');
|
||||
return _CheckNeedsReleaseResult.failure;
|
||||
}
|
||||
// Get latest tagged version and compare with the current version.
|
||||
// TODO(cyanglaz): Check latest version of the package on pub instead of git
|
||||
// https://github.com/flutter/flutter/issues/81047
|
||||
|
||||
final String latestTag = existingTags.firstWhere(
|
||||
(String tag) => tag.split('-v').first == pubspec.name,
|
||||
orElse: () => '');
|
||||
if (latestTag.isNotEmpty) {
|
||||
final String latestTaggedVersion = latestTag.split('-v').last;
|
||||
final Version latestVersion = Version.parse(latestTaggedVersion);
|
||||
if (pubspec.version < latestVersion) {
|
||||
_print(
|
||||
'The new version (${pubspec.version}) is lower than the current version ($latestVersion) for ${pubspec.name}.\nThis git commit is a revert, no release is tagged.');
|
||||
return _CheckNeedsReleaseResult.noRelease;
|
||||
}
|
||||
}
|
||||
return _CheckNeedsReleaseResult.release;
|
||||
}
|
||||
|
||||
// Publish the plugin.
|
||||
//
|
||||
// Returns `true` if successful, `false` otherwise.
|
||||
Future<bool> _publishPlugin({@required Directory packageDir}) async {
|
||||
final bool gitStatusOK = await _checkGitStatus(packageDir);
|
||||
if (!gitStatusOK) {
|
||||
return false;
|
||||
}
|
||||
final bool publishOK = await _publish(packageDir);
|
||||
if (!publishOK) {
|
||||
return false;
|
||||
}
|
||||
_print('Package published!');
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _tagRelease(
|
||||
{@required Directory packageDir,
|
||||
@required String remote,
|
||||
@required String remoteUrl,
|
||||
@required bool shouldPushTag}) async {
|
||||
// Tag the release with <plugin-name>-v<version>
|
||||
//
|
||||
// Return `true` if successful, `false` otherwise.
|
||||
Future<bool> _tagRelease({
|
||||
@required Directory packageDir,
|
||||
@required String remote,
|
||||
@required String remoteUrl,
|
||||
@required bool shouldPushTag,
|
||||
}) async {
|
||||
final String tag = _getTag(packageDir);
|
||||
_print('Tagging release $tag...');
|
||||
await processRunner.run(
|
||||
'git',
|
||||
<String>['tag', tag],
|
||||
workingDir: packageDir,
|
||||
exitOnError: true,
|
||||
logOnError: true,
|
||||
);
|
||||
if (!(argResults[_dryRunFlag] as bool)) {
|
||||
final io.ProcessResult result = await processRunner.run(
|
||||
'git',
|
||||
<String>['tag', tag],
|
||||
workingDir: packageDir,
|
||||
exitOnError: false,
|
||||
logOnError: true,
|
||||
);
|
||||
if (result.exitCode != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldPushTag) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
_print('Pushing tag to $remote...');
|
||||
await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl);
|
||||
return await _pushTagToRemote(
|
||||
remote: remote,
|
||||
tag: tag,
|
||||
remoteUrl: remoteUrl,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _finishSuccesfully() async {
|
||||
await _stdinSubscription.cancel();
|
||||
_print('Done!');
|
||||
Future<void> _finish(bool successful) async {
|
||||
if (_stdinSubscription != null) {
|
||||
await _stdinSubscription.cancel();
|
||||
_stdinSubscription = null;
|
||||
}
|
||||
if (successful) {
|
||||
_print('Done!');
|
||||
} else {
|
||||
_print('Failed, see above for details.');
|
||||
throw ToolExit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the packageDirectory based on the package name.
|
||||
@ -167,14 +370,17 @@ class PublishPluginCommand extends PluginCommand {
|
||||
return packageDir;
|
||||
}
|
||||
|
||||
Future<void> _checkGitStatus(Directory packageDir) async {
|
||||
final ProcessResult statusResult = await processRunner.run(
|
||||
Future<bool> _checkGitStatus(Directory packageDir) async {
|
||||
final io.ProcessResult statusResult = await processRunner.run(
|
||||
'git',
|
||||
<String>['status', '--porcelain', '--ignored', packageDir.absolute.path],
|
||||
workingDir: packageDir,
|
||||
logOnError: true,
|
||||
exitOnError: true,
|
||||
exitOnError: false,
|
||||
);
|
||||
if (statusResult.exitCode != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String statusOutput = statusResult.stdout as String;
|
||||
if (statusOutput.isNotEmpty) {
|
||||
@ -182,12 +388,12 @@ class PublishPluginCommand extends PluginCommand {
|
||||
"There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n"
|
||||
'$statusOutput\n'
|
||||
'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.');
|
||||
throw ToolExit(1);
|
||||
}
|
||||
return statusOutput.isEmpty;
|
||||
}
|
||||
|
||||
Future<String> _verifyRemote(String remote) async {
|
||||
final ProcessResult remoteInfo = await processRunner.run(
|
||||
final io.ProcessResult remoteInfo = await processRunner.run(
|
||||
'git',
|
||||
<String>['remote', 'get-url', remote],
|
||||
workingDir: packagesDir,
|
||||
@ -197,28 +403,31 @@ class PublishPluginCommand extends PluginCommand {
|
||||
return remoteInfo.stdout as String;
|
||||
}
|
||||
|
||||
Future<void> _publish(Directory packageDir) async {
|
||||
Future<bool> _publish(Directory packageDir) async {
|
||||
final List<String> publishFlags =
|
||||
argResults[_pubFlagsOption] as List<String>;
|
||||
_print(
|
||||
'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n');
|
||||
final Process publish = await processRunner.start(
|
||||
'flutter', <String>['pub', 'publish'] + publishFlags,
|
||||
workingDirectory: packageDir);
|
||||
publish.stdout
|
||||
.transform(utf8.decoder)
|
||||
.listen((String data) => _print(data));
|
||||
publish.stderr
|
||||
.transform(utf8.decoder)
|
||||
.listen((String data) => _print(data));
|
||||
_stdinSubscription = _stdin
|
||||
.transform(utf8.decoder)
|
||||
.listen((String data) => publish.stdin.writeln(data));
|
||||
final int result = await publish.exitCode;
|
||||
if (result != 0) {
|
||||
_print('Publish failed. Exiting.');
|
||||
throw ToolExit(result);
|
||||
if (!(argResults[_dryRunFlag] as bool)) {
|
||||
final io.Process publish = await processRunner.start(
|
||||
'flutter', <String>['pub', 'publish'] + publishFlags,
|
||||
workingDirectory: packageDir);
|
||||
publish.stdout
|
||||
.transform(utf8.decoder)
|
||||
.listen((String data) => _print(data));
|
||||
publish.stderr
|
||||
.transform(utf8.decoder)
|
||||
.listen((String data) => _print(data));
|
||||
_stdinSubscription ??= _stdin
|
||||
.transform(utf8.decoder)
|
||||
.listen((String data) => publish.stdin.writeln(data));
|
||||
final int result = await publish.exitCode;
|
||||
if (result != 0) {
|
||||
_print('Publish ${packageDir.basename} failed.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String _getTag(Directory packageDir) {
|
||||
@ -235,23 +444,44 @@ class PublishPluginCommand extends PluginCommand {
|
||||
.replaceAll('%VERSION%', version);
|
||||
}
|
||||
|
||||
Future<void> _pushTagToRemote(
|
||||
{@required String remote,
|
||||
@required String tag,
|
||||
@required String remoteUrl}) async {
|
||||
// Pushes the `tag` to `remote`
|
||||
//
|
||||
// Return `true` if successful, `false` otherwise.
|
||||
Future<bool> _pushTagToRemote({
|
||||
@required String remote,
|
||||
@required String tag,
|
||||
@required String remoteUrl,
|
||||
}) async {
|
||||
assert(remote != null && tag != null && remoteUrl != null);
|
||||
_print('Ready to push $tag to $remoteUrl (y/n)?');
|
||||
final String input = _stdin.readLineSync();
|
||||
if (input.toLowerCase() != 'y') {
|
||||
_print('Tag push canceled.');
|
||||
throw ToolExit(1);
|
||||
return false;
|
||||
}
|
||||
await processRunner.run(
|
||||
'git',
|
||||
<String>['push', remote, tag],
|
||||
workingDir: packagesDir,
|
||||
exitOnError: true,
|
||||
logOnError: true,
|
||||
);
|
||||
if (!(argResults[_dryRunFlag] as bool)) {
|
||||
final io.ProcessResult result = await processRunner.run(
|
||||
'git',
|
||||
<String>['push', remote, tag],
|
||||
workingDir: packagesDir,
|
||||
exitOnError: false,
|
||||
logOnError: true,
|
||||
);
|
||||
if (result.exitCode != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
enum _CheckNeedsReleaseResult {
|
||||
// The package needs to be released.
|
||||
release,
|
||||
|
||||
// The package does not need to be released.
|
||||
noRelease,
|
||||
|
||||
// There's an error when trying to determine whether the package needs to be released.
|
||||
failure,
|
||||
}
|
||||
|
Reference in New Issue
Block a user