From e674e0dc2b23f33ea2f5072cb6853d6b1cf6de85 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 11 May 2021 14:39:58 -0700 Subject: [PATCH] [tool] add an `skip-confirmation` flag to publish-package for running the command on ci (#3842) the skip-confirmation flag will add a --force flag to pub publish, it will also let users to skip the y/n question when pushing tags to remote. Fixes flutter/flutter#79830 --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/publish_plugin_command.dart | 115 ++++++++++++++---- .../test/publish_plugin_command_test.dart | 112 ++++++++++++----- 3 files changed, 171 insertions(+), 57 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index bc2a775db1f8..5e9ce9946835 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,7 @@ - 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. +- Add `skip-conformation` flag to publish-plugin to allow auto publishing. ## 0.1.1 diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 9bfa0e71743a..1c8a2dc57525 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -84,6 +84,12 @@ class PublishPluginCommand extends PluginCommand { defaultsTo: false, negatable: true, ); + argParser.addFlag(_skipConfirmationFlag, + help: 'Run the command without asking for Y/N inputs.\n' + 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n' + 'It also skips the y/n inputs when pushing tags to remote.\n', + defaultsTo: false, + negatable: true); } static const String _packageOption = 'package'; @@ -93,6 +99,9 @@ class PublishPluginCommand extends PluginCommand { static const String _remoteOption = 'remote'; static const String _allChangedFlag = 'all-changed'; static const String _dryRunFlag = 'dry-run'; + static const String _skipConfirmationFlag = 'skip-confirmation'; + + static const String _pubCredentialName = 'PUB_CREDENTIALS'; // Version tags should follow -v. For example, // `flutter_plugin_tools-v0.0.24`. @@ -103,7 +112,9 @@ class PublishPluginCommand extends PluginCommand { @override final String description = - 'Attempts to publish the given plugin and tag its release on GitHub.'; + 'Attempts to publish the given plugin and tag its release on GitHub.\n' + 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' + 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; final Print _print; final io.Stdin _stdin; @@ -267,7 +278,8 @@ Safe to ignore if the package is deleted in this commit. } if (pubspec.version == null) { - _print('No version found. A package that intentionally has no version should be marked "publish_to: none"'); + _print( + 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); return _CheckNeedsReleaseResult.failure; } @@ -408,24 +420,33 @@ Safe to ignore if the package is deleted in this commit. argResults[_pubFlagsOption] as List; _print( 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); - if (!(argResults[_dryRunFlag] as bool)) { - final io.Process publish = await processRunner.start( - 'flutter', ['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; - } + if (argResults[_dryRunFlag] as bool) { + return true; + } + + if (argResults[_skipConfirmationFlag] as bool) { + publishFlags.add('--force'); + } + if (publishFlags.contains('--force')) { + _ensureValidPubCredential(); + } + + final io.Process publish = await processRunner.start( + 'flutter', ['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; } @@ -453,11 +474,13 @@ Safe to ignore if the package is deleted in this commit. @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.'); - return false; + if (!(argResults[_skipConfirmationFlag] as bool)) { + _print('Ready to push $tag to $remoteUrl (y/n)?'); + final String input = _stdin.readLineSync(); + if (input.toLowerCase() != 'y') { + _print('Tag push canceled.'); + return false; + } } if (!(argResults[_dryRunFlag] as bool)) { final io.ProcessResult result = await processRunner.run( @@ -473,8 +496,50 @@ Safe to ignore if the package is deleted in this commit. } return true; } + + void _ensureValidPubCredential() { + final File credentialFile = fileSystem.file(_credentialsPath); + if (credentialFile.existsSync() && + credentialFile.readAsStringSync().isNotEmpty) { + return; + } + final String credential = io.Platform.environment[_pubCredentialName]; + if (credential == null) { + printErrorAndExit(errorMessage: ''' +No pub credential available. Please check if `~/.pub-cache/credentials.json` is valid. +If running this command on CI, you can set the pub credential content in the $_pubCredentialName environment variable. +'''); + } + credentialFile.openSync(mode: FileMode.writeOnlyAppend) + ..writeStringSync(credential) + ..closeSync(); + } + + /// Returns the correct path where the pub credential is stored. + @visibleForTesting + static String getCredentialPath() { + return _credentialsPath; + } } +/// The path in which pub expects to find its credentials file. +final String _credentialsPath = () { + // This follows the same logic as pub: + // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 + String cacheDir; + final String pubCache = io.Platform.environment['PUB_CACHE']; + print(pubCache); + if (pubCache != null) { + cacheDir = pubCache; + } else if (io.Platform.isWindows) { + final String appData = io.Platform.environment['APPDATA']; + cacheDir = p.join(appData, 'Pub', 'Cache'); + } else { + cacheDir = p.join(io.Platform.environment['HOME'], '.pub-cache'); + } + return p.join(cacheDir, 'credentials.json'); +}(); + enum _CheckNeedsReleaseResult { // The package needs to be released. release, diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 02d6d8140a6e..b622e05861fa 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -29,32 +29,39 @@ void main() { TestProcessRunner processRunner; CommandRunner commandRunner; MockStdin mockStdin; + // This test uses a local file system instead of an in memory one throughout + // so that git actually works. In setup we initialize a mono repo of plugins + // with one package and commit everything to Git. + const FileSystem fileSystem = LocalFileSystem(); + + void _createMockCredentialFile() { + final String credentialPath = PublishPluginCommand.getCredentialPath(); + fileSystem.file(credentialPath) + ..createSync(recursive: true) + ..writeAsStringSync('some credential'); + } setUp(() async { - // This test uses a local file system instead of an in memory one throughout - // so that git actually works. In setup we initialize a mono repo of plugins - // with one package and commit everything to Git. - parentDir = const LocalFileSystem() - .systemTempDirectory + parentDir = fileSystem.systemTempDirectory .createTempSync('publish_plugin_command_test-'); initializeFakePackages(parentDir: parentDir); - pluginDir = createFakePlugin(testPluginName, withSingleExample: false); + pluginDir = createFakePlugin(testPluginName, + withSingleExample: false, packagesDirectory: parentDir); assert(pluginDir != null && pluginDir.existsSync()); createFakePubspec(pluginDir, includeVersion: true); io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); + workingDirectory: parentDir.path); + gitDir = await GitDir.fromExisting(parentDir.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Initial commit']); processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand( - mockPackagesDir, mockPackagesDir.fileSystem, + ..addCommand(PublishPluginCommand(parentDir, fileSystem, processRunner: processRunner, print: (Object message) => printedMessages.add(message.toString()), stdinput: mockStdin, - gitDir: await GitDir.fromExisting(mockPackagesDir.path))); + gitDir: await GitDir.fromExisting(parentDir.path))); }); tearDown(() { @@ -129,8 +136,8 @@ void main() { test('can publish non-flutter package', () async { createFakePubspec(pluginDir, includeVersion: true, isFlutter: false); io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); + workingDirectory: parentDir.path); + gitDir = await GitDir.fromExisting(parentDir.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. @@ -202,6 +209,29 @@ void main() { expect(processRunner.mockPublishArgs[3], '--server=foo'); }); + test( + '--skip-confirmation flag automatically adds --force to --pub-publish-flags', + () async { + processRunner.mockPublishCompleteCode = 0; + _createMockCredentialFile(); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + '--skip-confirmation', + '--pub-publish-flags', + '--server=foo' + ]); + + expect(processRunner.mockPublishArgs.length, 4); + expect(processRunner.mockPublishArgs[0], 'pub'); + expect(processRunner.mockPublishArgs[1], 'publish'); + expect(processRunner.mockPublishArgs[2], '--server=foo'); + expect(processRunner.mockPublishArgs[3], '--force'); + }); + test('throws if pub publish fails', () async { processRunner.mockPublishCompleteCode = 128; await expectLater( @@ -312,6 +342,24 @@ void main() { expect(printedMessages.last, 'Done!'); }); + test('does not ask for user input if the --skip-confirmation flag is on', + () async { + await gitDir.runCommand(['tag', 'garbage']); + processRunner.mockPublishCompleteCode = 0; + _createMockCredentialFile(); + await commandRunner.run([ + 'publish-plugin', + '--skip-confirmation', + '--package', + testPluginName, + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + test('to upstream by default, dry run', () async { await gitDir.runCommand(['tag', 'garbage']); // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. @@ -368,8 +416,8 @@ void main() { group('Auto release (all-changed flag)', () { setUp(() async { io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); + workingDirectory: parentDir.path); + gitDir = await GitDir.fromExisting(parentDir.path); await gitDir.runCommand( ['remote', 'add', 'upstream', 'http://localhost:8000']); }); @@ -377,12 +425,12 @@ void main() { test('can release newly created plugins', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -424,7 +472,7 @@ void main() { () async { // Prepare an exiting plugin and tag it final Directory pluginDir0 = createFakePlugin('plugin0', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); createFakePubspec(pluginDir0, name: 'plugin0', includeVersion: true, @@ -441,12 +489,12 @@ void main() { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -485,12 +533,12 @@ void main() { test('can release newly created plugins, dry run', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -534,12 +582,12 @@ void main() { test('version change triggers releases.', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -625,12 +673,12 @@ void main() { () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -713,12 +761,12 @@ void main() { () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -795,12 +843,12 @@ void main() { test('No version change does not release any plugins', () async { // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', - withSingleExample: true, packagesDirectory: mockPackagesDir); + withSingleExample: true, packagesDirectory: parentDir); // federated final Directory pluginDir2 = createFakePlugin('plugin2', withSingleExample: true, parentDirectoryName: 'plugin2', - packagesDirectory: mockPackagesDir); + packagesDirectory: parentDir); createFakePubspec(pluginDir1, name: 'plugin1', includeVersion: true, @@ -813,8 +861,8 @@ void main() { version: '0.0.1'); io.Process.runSync('git', ['init'], - workingDirectory: mockPackagesDir.path); - gitDir = await GitDir.fromExisting(mockPackagesDir.path); + workingDirectory: parentDir.path); + gitDir = await GitDir.fromExisting(parentDir.path); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']);