From 49274f1e3958a017ae6eaf37ca32d3c087ae7e80 Mon Sep 17 00:00:00 2001 From: Koen Van Looveren Date: Fri, 8 Mar 2024 16:26:32 +0100 Subject: [PATCH] fix: improved logging for the cli with cli_spin --- bin/impaktfull_cli.dart | 2 +- lib/src/core/cli_constants.dart | 11 + .../command/command/impaktfull_command.dart | 3 +- .../error/impaktfull_cli_argument_error.dart | 20 +- .../error/impaktfull_cli_exit_error.dart | 5 + .../impaktfull_cli_process_runner_error.dart | 10 + .../args/env/impaktfull_cli_environment.dart | 59 ++---- .../force_quit_listener.dart} | 11 +- .../versbose_logging_listener.dart | 24 +++ .../impaktfull_cli_input_reader.dart | 2 +- lib/src/core/util/logger/logger.dart | 197 +++++++++++++++--- lib/src/core/util/runner/runner.dart | 21 +- lib/src/impaktfull_cli.dart | 46 ++-- .../appcenter/plugin/appcenter_plugin.dart | 75 +++---- .../plugin/mac_os_keychain_plugin.dart | 60 ++---- .../ci_cd/plugin/ci_cd_plugin.dart | 18 +- .../build/plugin/flutter_build_plugin.dart | 31 +-- .../plugin/one_password_plugin.dart | 29 ++- .../model/playstore_upload_config.dart | 24 +++ .../playstore/plugin/playstore_plugin.dart | 175 +++++++++++----- .../testflight/plugin/testflight_plugin.dart | 29 ++- pubspec.yaml | 1 + 22 files changed, 539 insertions(+), 314 deletions(-) create mode 100644 lib/src/core/cli_constants.dart create mode 100644 lib/src/core/model/error/impaktfull_cli_exit_error.dart create mode 100644 lib/src/core/model/error/impaktfull_cli_process_runner_error.dart rename lib/src/core/util/{cli/force_quit_util.dart => input_listener/force_quit_listener.dart} (76%) create mode 100644 lib/src/core/util/input_listener/versbose_logging_listener.dart diff --git a/bin/impaktfull_cli.dart b/bin/impaktfull_cli.dart index 53d4f0c..648bf71 100644 --- a/bin/impaktfull_cli.dart +++ b/bin/impaktfull_cli.dart @@ -1,3 +1,3 @@ import 'package:impaktfull_cli/src/impaktfull_cli.dart'; -Future main(List arguments) => ImpaktfullCli().runCli(arguments); +Future main(List arguments) => ImpaktfullCli(arguments: arguments).runCli(); diff --git a/lib/src/core/cli_constants.dart b/lib/src/core/cli_constants.dart new file mode 100644 index 0000000..68f7232 --- /dev/null +++ b/lib/src/core/cli_constants.dart @@ -0,0 +1,11 @@ +import 'dart:io'; + +import 'package:path/path.dart'; + +class CliConstants { + CliConstants._(); + + static final buildFolder = Directory(join('build', 'impaktfull_cli')); + + static String get buildFolderPath => buildFolder.path; +} diff --git a/lib/src/core/command/command/impaktfull_command.dart b/lib/src/core/command/command/impaktfull_command.dart index d2b812d..5b6523a 100644 --- a/lib/src/core/command/command/impaktfull_command.dart +++ b/lib/src/core/command/command/impaktfull_command.dart @@ -19,8 +19,7 @@ abstract class ImpaktfullCommand extends Command { } on ArgumentError catch (e) { throw ImpaktfullCliArgumentError( e.message.toString(), - argResults?.name, - argParser.usage, + argParser: argParser, ); } await runCommand(configData); diff --git a/lib/src/core/model/error/impaktfull_cli_argument_error.dart b/lib/src/core/model/error/impaktfull_cli_argument_error.dart index 48bb926..7e9fcfc 100644 --- a/lib/src/core/model/error/impaktfull_cli_argument_error.dart +++ b/lib/src/core/model/error/impaktfull_cli_argument_error.dart @@ -1,21 +1,11 @@ +import 'package:args/args.dart'; import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_error.dart'; class ImpaktfullCliArgumentError extends ImpaktfullCliError { - final String? command; - final String usage; + final ArgParser argParser; ImpaktfullCliArgumentError( - super.message, - this.command, - this.usage, - ); - - @override - String toString() => '''Error while parsing arguments for `$command`: - -$message - -Usage fo `$command`: -$usage -'''; + super.message, { + required this.argParser, + }); } diff --git a/lib/src/core/model/error/impaktfull_cli_exit_error.dart b/lib/src/core/model/error/impaktfull_cli_exit_error.dart new file mode 100644 index 0000000..e1fbfdc --- /dev/null +++ b/lib/src/core/model/error/impaktfull_cli_exit_error.dart @@ -0,0 +1,5 @@ +import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_error.dart'; + +class ImpktfullCliExitError extends ImpaktfullCliError { + ImpktfullCliExitError(super.message); +} diff --git a/lib/src/core/model/error/impaktfull_cli_process_runner_error.dart b/lib/src/core/model/error/impaktfull_cli_process_runner_error.dart new file mode 100644 index 0000000..d5f16dd --- /dev/null +++ b/lib/src/core/model/error/impaktfull_cli_process_runner_error.dart @@ -0,0 +1,10 @@ +import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_error.dart'; + +class ImpaktfullCliProcessRunnerError extends ImpaktfullCliError { + final String errorOutput; + + ImpaktfullCliProcessRunnerError( + super.message, + this.errorOutput, + ); +} diff --git a/lib/src/core/util/args/env/impaktfull_cli_environment.dart b/lib/src/core/util/args/env/impaktfull_cli_environment.dart index 8958471..c1115fd 100644 --- a/lib/src/core/util/args/env/impaktfull_cli_environment.dart +++ b/lib/src/core/util/args/env/impaktfull_cli_environment.dart @@ -11,21 +11,17 @@ import 'package:path/path.dart'; class ImpaktfullCliEnvironment { static late ImpaktfullCliEnvironment _instance; - final bool verboseLoggingEnabled; final Directory workingDirectory; final bool isFvmProject; final List allCliTools; static ImpaktfullCliEnvironment get instance => _instance; - List get installedCliTools => - allCliTools.where((element) => element.isInstalled).toList(); + List get installedCliTools => allCliTools.where((element) => element.isInstalled).toList(); - List get notInstalledCliTools => - allCliTools.where((element) => !element.isInstalled).toList(); + List get notInstalledCliTools => allCliTools.where((element) => !element.isInstalled).toList(); const ImpaktfullCliEnvironment._({ - required this.verboseLoggingEnabled, required this.workingDirectory, required this.isFvmProject, required this.allCliTools, @@ -33,12 +29,9 @@ class ImpaktfullCliEnvironment { static Future init({ ProcessRunner processRunner = const CliProcessRunner(), - bool isVerboseLoggingEnabled = false, }) async { - ImpaktfullCliLogger.init(isVerboseLoggingEnabled: isVerboseLoggingEnabled); final workingDir = Directory.current; _instance = ImpaktfullCliEnvironment._( - verboseLoggingEnabled: isVerboseLoggingEnabled, workingDirectory: workingDir, isFvmProject: await _checkIfActiveProjectIsFvm(workingDir), allCliTools: await _checkInstalledTools(processRunner), @@ -51,18 +44,11 @@ class ImpaktfullCliEnvironment { return fvmConfigFile.exists(); } - static Future> _checkInstalledTools( - ProcessRunner processRunner) async => - CliTool.values - .where((element) => element.supportedOperatingSystems - .contains(OperatingSystem.current)) - .map((cliTool) => _isToolInstalled(processRunner, cliTool)) - .wait; + static Future> _checkInstalledTools(ProcessRunner processRunner) async => + CliTool.values.where((element) => element.supportedOperatingSystems.contains(OperatingSystem.current)).map((cliTool) => _isToolInstalled(processRunner, cliTool)).wait; - static Future _isToolInstalled( - ProcessRunner processRunner, CliTool cliTool) async { - final result = - await processRunner.runProcess(['which', cliTool.commandName]); + static Future _isToolInstalled(ProcessRunner processRunner, CliTool cliTool) async { + final result = await processRunner.runProcess(['which', cliTool.commandName]); if (result.isEmpty) return InstalledCliTool.notInstalled(cliTool: cliTool); return InstalledCliTool.installed( cliTool: cliTool, @@ -76,8 +62,7 @@ class ImpaktfullCliEnvironment { } } - static bool isInstalled(CliTool cliTool) => - _instance.allCliTools.any((element) => element.cliTool == cliTool); + static bool isInstalled(CliTool cliTool) => _instance.allCliTools.any((element) => element.cliTool == cliTool); static void requiresInstalledTools(List requiredTools) { final requiredToolsFound = []; @@ -88,40 +73,34 @@ class ImpaktfullCliEnvironment { } } if (requiredToolsFound.length != requiredTools.length) { - final missingTools = requiredTools - .where((element) => !requiredToolsFound.contains(element)); - throw ImpaktfullCliError( - '${missingTools.map((e) => '${e.commandName} (${e.name})').join(', ')} are not installed, but required for the next step'); + final missingTools = requiredTools.where((element) => !requiredToolsFound.contains(element)); + throw ImpaktfullCliError('${missingTools.map((e) => '${e.commandName} (${e.name})').join(', ')} are not installed, but required for the next step'); } } static void _printCurrentState() { - ImpaktfullCliLogger.debugSeperator(); - ImpaktfullCliLogger.debug( - 'Operating system: ${OperatingSystem.current.name}'); - ImpaktfullCliLogger.debug( - 'Working Dir: `${_instance.workingDirectory.path}`'); - ImpaktfullCliLogger.debug('Is fvm project: `${_instance.isFvmProject}`'); + ImpaktfullCliLogger.verboseSeperator(); + ImpaktfullCliLogger.verbose('Operating system: ${OperatingSystem.current.name}'); + ImpaktfullCliLogger.verbose('Working Dir: `${_instance.workingDirectory.path}`'); + ImpaktfullCliLogger.verbose('Is fvm project: `${_instance.isFvmProject}`'); if (_instance.installedCliTools.isNotEmpty) { - ImpaktfullCliLogger.debug('Installed Tools:'); + ImpaktfullCliLogger.verbose('Installed Tools:'); for (final clitool in _instance.installedCliTools) { - ImpaktfullCliLogger.debug( - '\t${clitool.cliTool.commandName} - ${clitool.path}'); + ImpaktfullCliLogger.verbose('\t${clitool.cliTool.commandName} - ${clitool.path}'); } } if (_instance.notInstalledCliTools.isNotEmpty) { - ImpaktfullCliLogger.debug('Not Installed Tools:'); + ImpaktfullCliLogger.verbose('Not Installed Tools:'); for (final notInstalledCliTool in _instance.notInstalledCliTools) { final cliTool = notInstalledCliTool.cliTool; var log = '\t${cliTool.commandName}'; - final installationInstructions = - cliTool.installationInstructions[OperatingSystem.current]; + final installationInstructions = cliTool.installationInstructions[OperatingSystem.current]; if (installationInstructions != null) { log += ' - Install instructions: $installationInstructions'; } - ImpaktfullCliLogger.debug(log); + ImpaktfullCliLogger.verbose(log); } } - ImpaktfullCliLogger.debugSeperator(); + ImpaktfullCliLogger.verboseSeperator(); } } diff --git a/lib/src/core/util/cli/force_quit_util.dart b/lib/src/core/util/input_listener/force_quit_listener.dart similarity index 76% rename from lib/src/core/util/cli/force_quit_util.dart rename to lib/src/core/util/input_listener/force_quit_listener.dart index 5eca8bd..c62890d 100644 --- a/lib/src/core/util/cli/force_quit_util.dart +++ b/lib/src/core/util/input_listener/force_quit_listener.dart @@ -5,7 +5,7 @@ import 'package:impaktfull_cli/impaktfull_cli.dart'; typedef AsyncCallback = Future Function(); -class ForceQuitUtil { +class ForceQuitListener { static var _isShuttingDown = false; static final _listeners = {}; @@ -13,17 +13,14 @@ class ForceQuitUtil { static void _addListener(AsyncCallback listener) => _listeners.add(listener); - static void _removeListener(AsyncCallback listener) => - _listeners.remove(listener); + static void _removeListener(AsyncCallback listener) => _listeners.remove(listener); static void init() { _subscription = ProcessSignal.sigint.watch().listen((signal) async { if (_isShuttingDown) return; _isShuttingDown = true; - ImpaktfullCliLogger.debug(''); - ImpaktfullCliLogger.debug('Force quit detected. Cleaning up...'); - ImpaktfullCliLogger.verbose( - 'Cleaning up ${_listeners.length} listeners...'); + ImpaktfullCliLogger.log('\nForce quit detected. Cleaning up...'); + ImpaktfullCliLogger.verbose('Cleaning up ${_listeners.length} listeners...'); await Future.wait(_listeners.map((e) => e())); exit(0); }); diff --git a/lib/src/core/util/input_listener/versbose_logging_listener.dart b/lib/src/core/util/input_listener/versbose_logging_listener.dart new file mode 100644 index 0000000..2b5a86c --- /dev/null +++ b/lib/src/core/util/input_listener/versbose_logging_listener.dart @@ -0,0 +1,24 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:impaktfull_cli/src/core/util/logger/logger.dart'; + +class VerboseLoggingListener { + static StreamSubscription>? _subscription; + + VerboseLoggingListener._(); + + static void startInputListener() { + _subscription = stdin.listen((data) { + final input = String.fromCharCodes(data).trim(); + + if (input.toLowerCase() == 'v' || input.toLowerCase() == 'verbose') { + ImpaktfullCliLogger.enableVerbose(isVerboseLoggingEnabled: true); + } else if (input.toLowerCase() == 'nv' || input.toLowerCase() == 'no-verbose') { + ImpaktfullCliLogger.enableVerbose(isVerboseLoggingEnabled: false); + } + }); + } + + static void stopListening() => _subscription?.cancel(); +} diff --git a/lib/src/core/util/input_reader/impaktfull_cli_input_reader.dart b/lib/src/core/util/input_reader/impaktfull_cli_input_reader.dart index 8c3b6a5..c0b54bb 100644 --- a/lib/src/core/util/input_reader/impaktfull_cli_input_reader.dart +++ b/lib/src/core/util/input_reader/impaktfull_cli_input_reader.dart @@ -8,7 +8,7 @@ class CliInputReader { const CliInputReader._(); static Secret readSecret(String message) { - ImpaktfullCliLogger.debug('$message:'); + ImpaktfullCliLogger.log('$message:'); stdin.echoMode = false; final secretValue = stdin.readLineSync(); if (secretValue == null || secretValue.isEmpty) { diff --git a/lib/src/core/util/logger/logger.dart b/lib/src/core/util/logger/logger.dart index 17d26b3..ecff3f9 100644 --- a/lib/src/core/util/logger/logger.dart +++ b/lib/src/core/util/logger/logger.dart @@ -1,28 +1,65 @@ -import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_error.dart'; -import 'package:impaktfull_cli/src/core/util/args/env/impaktfull_cli_environment_variables.dart'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:cli_spin/cli_spin.dart'; +import 'package:impaktfull_cli/impaktfull_cli.dart'; +import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_exit_error.dart'; +import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_process_runner_error.dart'; +import 'package:path/path.dart'; class ImpaktfullCliLogger { + static late File _logFile; + static final _secrets = []; static bool _verbose = false; - static const String _separator = - '=========================================================================================='; + static final _pendingLogs = []; + static const String _seperator = '================================================================================'; + static const String _seperatorSingleLine = '--------------------------------------------------------------------------------'; + + static String? _cliSpinnerActionDescription; + static CliSpin? _cliSpinner; + + static String? _spinnerPrefix; + + static bool get isVerboseLoggingEnabled => _verbose; ImpaktfullCliLogger._(); - static void init({bool isVerboseLoggingEnabled = false}) { + static void init() { + _logFile = File(join(Directory.current.path, 'investsuite_cli.log')); + } + + static void enableVerbose({bool isVerboseLoggingEnabled = false}) { _verbose = isVerboseLoggingEnabled; - initSecrets(); if (isVerboseLoggingEnabled) { + _cliSpinner?.stop(); + _printPendingLogs(); ImpaktfullCliLogger.verbose('Verbose logging enabled'); + } else { + _cliSpinner?.start(); + } + } + + static void _printPendingLogs({bool toFile = false}) { + final pendingLogs = List.from(_pendingLogs); + _pendingLogs.clear(); + if (pendingLogs.isNotEmpty) { + if (toFile) { + _logFile.createSync(recursive: true); + _logFile.writeAsStringSync(pendingLogs.join('\n')); + } else { + for (final log in pendingLogs) { + _print(log); + } + } } } - static void log(String message) => debug(message); + static void log(String message) => _print(message); - static void debug(String message) => _print(message); - static void logSeperator() => log(_separator); - static void debugSeperator() => debug(_separator); - static void verboseSeperator() => verbose(_separator); + static void logSeperator({bool singleLine = false}) => log(singleLine ? _seperatorSingleLine : _seperator); + + static void verboseSeperator({bool singleLine = false}) => verbose(singleLine ? _seperatorSingleLine : _seperator); static void error(String message) { if (message.startsWith('[ERROR]')) { @@ -32,36 +69,148 @@ class ImpaktfullCliLogger { } } - static void argumentError(Object error) { + static void _logCliError(ImpaktfullCliError error) { final sb = StringBuffer(); sb.writeln(); - sb.writeln(_separator); - if (error is ImpaktfullCliError) { - sb.writeln('ImpaktfullCliError:'); - sb.writeln(error.message); + sb.writeln(_seperator); + if (error is ImpaktfullCliArgumentError) { + sb.writeln('CliArgumentError:'); } else { sb.writeln('Something went wrong:'); - sb.writeln(error.toString()); } - sb.writeln(_separator); - debug(sb.toString()); + if (error is ImpaktfullCliProcessRunnerError) { + final errorMessages = error.errorOutput.split('\n').where((element) => element.contains('error:')); + if (errorMessages.isNotEmpty) { + sb.writeln(); + sb.writeln('Possible logs we detected could be interesting to investigate:'); + sb.writeln(); + sb.writeln(); + for (final element in errorMessages) { + sb.writeln(element); + } + sb.writeln(); + sb.writeln(); + } + } + sb.writeln(error.message); + sb.writeln(_seperator); + if (error is ImpaktfullCliArgumentError) { + sb.writeln('Usage:'); + sb.writeln(error.argParser.usage); + } else { + sb.writeln(error.stackTrace); + } + log(sb.toString()); } static void verbose(String message) { - if (!_verbose) return; + if (!_verbose) { + _pendingLogs.add(message); + return; + } _print(message); } static void _print(String message) { var safeMessage = message; - if (!_verbose) { - for (final secret in _secrets) { - safeMessage = safeMessage.replaceAll(secret, '****'); - } + for (final secret in _secrets) { + safeMessage = safeMessage.replaceAll(secret, '****'); } print(safeMessage); } + // Input + static bool askQuestion(String title) { + log('\n$title (y/n)'); + bool? result; + do { + var line = stdin.readLineSync(encoding: utf8); + final allowedValues = ['y', 'n', 'yes', 'no']; + if (allowedValues.contains(line)) { + result = line == 'y' || line == 'yes'; + } else { + log('\nPlease enter y or n'); + } + } while (result == null); + return result; + } + + // Spinner + static void setSpinnerPrefix(String prefix) { + _spinnerPrefix = prefix; + } + + static void clearSpinnerPrefix({ + bool shouldEndSpinner = true, + }) { + if (shouldEndSpinner) { + endSpinner(); + } + _spinnerPrefix = null; + } + + static void startSpinner( + String actionDescription, { + bool overidePreviousSpinner = true, + bool skipPrefix = false, + }) { + if (_cliSpinnerActionDescription != null) { + if (!overidePreviousSpinner) { + throw ImpaktfullCliError('$_cliSpinnerActionDescription is still running, and `overidePreviousSpinner` is set to `false`'); + } + endSpinner(); + } + final fullDescription = _spinnerPrefix == null || skipPrefix ? actionDescription : '$_spinnerPrefix: $actionDescription'; + final message = 'Start `$fullDescription`'; + _cliSpinnerActionDescription = fullDescription; + _cliSpinner = CliSpin( + text: message, + ); + if (_verbose) { + log('⏳ $message'); + } else { + verbose('⏳ $message'); + _cliSpinner?.start(); + } + } + + static void failSpinner(dynamic e, StackTrace trace) { + _printPendingLogs(toFile: true); + if (_cliSpinnerActionDescription != null) { + final message = 'Failed to finish: `$_cliSpinnerActionDescription`'; + if (_verbose) { + log('❌ $message'); + } else { + verbose('❌ $message'); + _cliSpinner?.fail(message); + } + } + if (e is ImpktfullCliExitError) { + log('\nCli exited with the following message:\n\n\n${e.message}\n\n\n'); + return; + } + if (e is ImpaktfullCliError) { + _logCliError(e); + } else { + error('${e.toString()}\n${trace.toString()}'); + } + log('Full log can be found here: ${_logFile.path}'); + _cliSpinnerActionDescription = null; + _cliSpinner = null; + } + + static void endSpinner() { + final message = 'Successfully finished: `$_cliSpinnerActionDescription`'; + if (_verbose) { + log('✅ $message'); + } else { + _cliSpinner?.success(message); + } + _cliSpinner?.stop(); + _cliSpinnerActionDescription = null; + _cliSpinner = null; + } + static void saveSecret(String value) { if (value.isEmpty) return; _secrets.add(value); diff --git a/lib/src/core/util/runner/runner.dart b/lib/src/core/util/runner/runner.dart index 848b4f8..23cd354 100644 --- a/lib/src/core/util/runner/runner.dart +++ b/lib/src/core/util/runner/runner.dart @@ -9,34 +9,25 @@ import 'package:impaktfull_cli/src/core/util/extensions/duration_extensions.dart import 'package:impaktfull_cli/src/core/util/logger/logger.dart'; Future runImpaktfullCli( - Future Function() run, { - bool isVerboseLoggingEnabled = false, - Future Function(Object error, StackTrace trace)? onError, -}) async { + Future Function() run, +) async { try { final stopwatch = Stopwatch(); stopwatch.start(); - await ImpaktfullCliEnvironment.init( - isVerboseLoggingEnabled: isVerboseLoggingEnabled); + await ImpaktfullCliEnvironment.init(); await run(); stopwatch.stop(); - ImpaktfullCliLogger.log( - '✅ Success (You just saved ${stopwatch.elapsed.humanReadibleDuration})'); + ImpaktfullCliLogger.log('✅ Success (You just saved ${stopwatch.elapsed.humanReadibleDuration})'); } on UsageException catch (e) { ImpaktfullCliLogger.error(e.toString()); exit(64); // Exit code 64 means a usage error occurred. - } on ImpaktfullCliArgumentError catch (error, trace) { + } on ImpaktfullCliArgumentError catch (error) { ImpaktfullCliLogger.error(error.toString()); - await onError?.call(error, trace); exit(-1); - } on ImpaktfullCliError catch (error, trace) { + } on ImpaktfullCliError catch (error) { ImpaktfullCliLogger.error(error.message); - await onError?.call(error, trace); exit(-1); } on ForceQuitError catch (_) { // Ignore because `ForceQuitUtil` already cleaned up the process. - } catch (error, trace) { - if (onError == null) rethrow; - await onError.call(error, trace); } } diff --git a/lib/src/impaktfull_cli.dart b/lib/src/impaktfull_cli.dart index 69987f0..1828e6c 100644 --- a/lib/src/impaktfull_cli.dart +++ b/lib/src/impaktfull_cli.dart @@ -1,7 +1,7 @@ import 'package:args/command_runner.dart'; import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_error.dart'; import 'package:impaktfull_cli/src/core/plugin/impaktfull_plugin.dart'; -import 'package:impaktfull_cli/src/core/util/cli/force_quit_util.dart'; +import 'package:impaktfull_cli/src/core/util/input_listener/force_quit_listener.dart'; import 'package:impaktfull_cli/src/integrations/appcenter/plugin/appcenter_plugin.dart'; import 'package:impaktfull_cli/src/integrations/apple_certificate/command/apple_certificate_root_command.dart'; import 'package:impaktfull_cli/src/core/util/extensions/arg_parser_extensions.dart'; @@ -16,16 +16,16 @@ import 'package:impaktfull_cli/src/integrations/one_password/plugin/one_password import 'package:impaktfull_cli/src/integrations/playstore/plugin/playstore_plugin.dart'; import 'package:impaktfull_cli/src/integrations/testflight/plugin/testflight_plugin.dart'; -typedef ImpaktfullCliRunner = Future Function( - T cli); +typedef ImpaktfullCliRunner = Future Function(T cli); class ImpaktfullCli { final ProcessRunner processRunner; - + final List arguments; late final Set _defaultPlugins; late final Set> _commands; ImpaktfullCli({ + this.arguments = const [], this.processRunner = const CliProcessRunner(), }); @@ -47,6 +47,8 @@ class ImpaktfullCli { Set get plugins => {}; + bool get isVerboseLoggingEnabled => arguments.contains('-v'); + T _getPlugin() { var plugin = _defaultPlugins.whereType().firstOrNull; plugin ??= plugins.whereType().firstOrNull; @@ -55,13 +57,17 @@ class ImpaktfullCli { } void init() { + ImpaktfullCliLogger.startSpinner('Initializing the cli'); + ImpaktfullCliLogger.init(); + ImpaktfullCliLogger.enableVerbose(isVerboseLoggingEnabled: isVerboseLoggingEnabled); _initCommands(); _initPlugins(); - ForceQuitUtil.init(); + ForceQuitListener.init(); + ImpaktfullCliLogger.endSpinner(); } void dispose() { - ForceQuitUtil.stopListening(); + ForceQuitListener.stopListening(); } void _initCommands() { @@ -72,8 +78,7 @@ class ImpaktfullCli { void _initPlugins() { final onePasswordPlugin = OnePasswordPlugin(processRunner: processRunner); - final macOsKeyChainPlugin = - MacOsKeyChainPlugin(processRunner: processRunner); + final macOsKeyChainPlugin = MacOsKeyChainPlugin(processRunner: processRunner); final flutterBuildPlugin = FlutterBuildPlugin(processRunner: processRunner); final appCenterPlugin = AppCenterPlugin(); final testflightPlugin = TestFlightPlugin(processRunner: processRunner); @@ -97,31 +102,28 @@ class ImpaktfullCli { } Future run( - ImpaktfullCliRunner runner, { - bool isVerboseLoggingEnabled = false, - }) async { + ImpaktfullCliRunner runner, + ) async { init(); - await runImpaktfullCli( - () => runner(this), - isVerboseLoggingEnabled: isVerboseLoggingEnabled, - ); + await runImpaktfullCli(() => runner(this)); dispose(); } - Future runCli(List args) async { + Future runCli() async { init(); await runImpaktfullCli(() async { - final runner = CommandRunner('impaktfull_cli', - 'A cli that replaces `fastlane` by simplifying the CI/CD process.'); + final runner = CommandRunner( + 'impaktfull_cli', + 'A cli that replaces `fastlane` by simplifying the CI/CD process.', + ); runner.argParser.addGlobalFlags(); for (final command in commands) { runner.addCommand(command); } - final argResults = runner.argParser.parse(args); - ImpaktfullCliLogger.init( - isVerboseLoggingEnabled: argResults.isVerboseLoggingEnabled()); - await runner.run(args); + final argResults = runner.argParser.parse(arguments); + ImpaktfullCliLogger.enableVerbose(isVerboseLoggingEnabled: argResults.isVerboseLoggingEnabled()); + await runner.run(arguments); }); dispose(); } diff --git a/lib/src/integrations/appcenter/plugin/appcenter_plugin.dart b/lib/src/integrations/appcenter/plugin/appcenter_plugin.dart index 8cc8faf..e2f4177 100644 --- a/lib/src/integrations/appcenter/plugin/appcenter_plugin.dart +++ b/lib/src/integrations/appcenter/plugin/appcenter_plugin.dart @@ -1,4 +1,5 @@ import 'package:http/http.dart' as http; +import 'package:impaktfull_cli/src/core/cli_constants.dart'; import 'package:impaktfull_cli/src/core/model/data/secret.dart'; import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_error.dart'; import 'package:impaktfull_cli/src/core/plugin/impaktfull_plugin.dart'; @@ -11,8 +12,7 @@ import 'dart:io'; const _appCenterApiBaseUrl = 'https://api.appcenter.ms/v0.1'; const _appCenterFilesBaseUrl = 'https://file.appcenter.ms'; -String _tempDirectoryPath = - join("build", 'impaktfull_cli', 'appcenter', 'upload'); +String _tempDirectoryPath = join(CliConstants.buildFolderPath, 'appcenter', 'upload'); const _extensionMimeTypeMapper = { 'apk': 'application/vnd.android.package-archive', 'aab': 'application/x-authorware-bin', @@ -34,28 +34,26 @@ class AppCenterPlugin extends ImpaktfullPlugin { List distributionGroups = defaultDistributionGroups, bool notifyListeners = true, }) async { - final ownerNameValue = - ownerName ?? ImpaktfullCliEnvironmentVariables.getAppCenterOwnerName(); - final apiTokenSecret = - apiToken ?? ImpaktfullCliEnvironmentVariables.getAppCenterToken(); + ImpaktfullCliLogger.setSpinnerPrefix('AppCenter upload'); + ImpaktfullCliLogger.startSpinner('Initializing'); + final ownerNameValue = ownerName ?? ImpaktfullCliEnvironmentVariables.getAppCenterOwnerName(); + final apiTokenSecret = apiToken ?? ImpaktfullCliEnvironmentVariables.getAppCenterToken(); - // =========CREATE NEW RELEASE========== + ImpaktfullCliLogger.startSpinner('Create new release'); final createNewReleaseUploadResponse = await _createNewRelease( appName: appName, ownerName: ownerNameValue, apiToken: apiTokenSecret, ); final id = createNewReleaseUploadResponse['id'] as String; - final packageAssetId = - createNewReleaseUploadResponse['package_asset_id'] as String; - final urlEncodedToken = - createNewReleaseUploadResponse['url_encoded_token'] as String; + final packageAssetId = createNewReleaseUploadResponse['package_asset_id'] as String; + final urlEncodedToken = createNewReleaseUploadResponse['url_encoded_token'] as String; final fileSizeBytes = await file.length(); final appType = _getAppTypeFor(file); ImpaktfullCliLogger.verbose('Detected: $appType meme type fo ${file.path}'); - // =========UPLOAD METADATA TO GET CHUNK SIZE========== + ImpaktfullCliLogger.startSpinner('Upload to get chunk size'); final chunkSize = await _uploadMetadata( packageAssetId: packageAssetId, file: file, @@ -65,6 +63,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { apiToken: apiTokenSecret, ); + ImpaktfullCliLogger.startSpinner('Splitting file into chunks'); // =========CREATE FOLDER TEMP/SPLIT TO STORAGE LIST CHUNK FILE AFTER SPLIT========== final tempDirectory = Directory(join(_tempDirectoryPath, 'split')); if (!tempDirectory.existsSync()) { @@ -86,6 +85,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { appType: appType, ); + ImpaktfullCliLogger.startSpinner('Confirming upload'); // =========FINISHED========== await _finishUpload( packageAssetId: packageAssetId, @@ -93,6 +93,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { apiToken: apiTokenSecret, ); + ImpaktfullCliLogger.startSpinner('Committing release'); // =========COMMIT========== await _commitRelease( id: id, @@ -101,6 +102,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { apiToken: apiTokenSecret, ); + ImpaktfullCliLogger.startSpinner('Checking if release is ready'); // =========POLL RESULT========== final releaseId = await _validateRelease( id: id, @@ -109,6 +111,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { apiToken: apiTokenSecret, ); + ImpaktfullCliLogger.startSpinner('Distribute release to correct groups'); // =========DISTRIBUTE========== final distributionUrl = await _distributeRelease( appName: appName, @@ -118,6 +121,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { notifyListeners: notifyListeners, apiToken: apiTokenSecret, ); + ImpaktfullCliLogger.clearSpinnerPrefix(); ImpaktfullCliLogger.logSeperator(); ImpaktfullCliLogger.log('Release $releaseId is ready!!! 🎉🎉🎉'); ImpaktfullCliLogger.log(distributionUrl); @@ -131,8 +135,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { required Secret apiToken, }) async { final response = await http.post( - Uri.parse( - "$_appCenterApiBaseUrl/apps/$ownerName/$appName/uploads/releases"), + Uri.parse("$_appCenterApiBaseUrl/apps/$ownerName/$appName/uploads/releases"), headers: _getHeaders(apiToken), ); if (response.statusCode != HttpStatus.created) { @@ -151,8 +154,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { required Secret apiToken, }) async { final fileName = basename(file.path); - final metaDataUrl = - "$_appCenterFilesBaseUrl/upload/set_metadata/$packageAssetId?file_name=$fileName&file_size=$fileSize&token=$urlEncodedToken&content_type=$appType"; + final metaDataUrl = "$_appCenterFilesBaseUrl/upload/set_metadata/$packageAssetId?file_name=$fileName&file_size=$fileSize&token=$urlEncodedToken&content_type=$appType"; final response = await http.post( Uri.parse(metaDataUrl), headers: _getHeaders(apiToken), @@ -182,16 +184,13 @@ class AppCenterPlugin extends ImpaktfullPlugin { while (chunksUsed < chunks.length) { chunksIndex++; - final newChunksUsed = chunksUsed + chunkSize > chunks.length - ? chunks.length - : chunksUsed + chunkSize; + final newChunksUsed = chunksUsed + chunkSize > chunks.length ? chunks.length : chunksUsed + chunkSize; final chunk = chunks.sublist(chunksUsed, newChunksUsed); - final chunkFile = - File(join(outputDirectory.path, 'chunk_$chunksIndex.apk')); + final chunkFile = File(join(outputDirectory.path, 'chunk_$chunksIndex.apk')); await chunkFile.writeAsBytes(chunk); chunksUsed = newChunksUsed; } - ImpaktfullCliLogger.debug('Split the file into $chunksIndex chunks'); + ImpaktfullCliLogger.verbose('Split the file into $chunksIndex chunks'); } Future _uploadChunks({ @@ -209,11 +208,10 @@ class AppCenterPlugin extends ImpaktfullPlugin { final fileName = basenameWithoutExtension(file.path); final blockNumber = int.parse(fileName.split('_').last); final contentLength = file.lengthSync(); - final uploadChunkurl = - "$_appCenterFilesBaseUrl/upload/upload_chunk/$packageAssetId?token=$urlEncodedToken&block_number=$blockNumber"; + final uploadChunkurl = "$_appCenterFilesBaseUrl/upload/upload_chunk/$packageAssetId?token=$urlEncodedToken&block_number=$blockNumber"; final chunkProgress = '$blockNumber/${tempFiles.length}'; - ImpaktfullCliLogger.log('Uploading chunks: $chunkProgress'); + ImpaktfullCliLogger.startSpinner('Uploading chunks: $chunkProgress'); final response = await http.post( Uri.parse(uploadChunkurl), @@ -226,8 +224,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { if (response.statusCode != HttpStatus.ok) { ImpaktfullCliLogger.verbose(response.body); - throw ImpaktfullCliError( - 'Failed to upload chunk ($chunkProgress) to AppCenter'); + throw ImpaktfullCliError('Failed to upload chunk ($chunkProgress) to AppCenter'); } } } @@ -237,8 +234,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { required String urlEncodedToken, required Secret apiToken, }) async { - final finishedUrl = - "$_appCenterFilesBaseUrl/upload/finished/$packageAssetId?token=$urlEncodedToken"; + final finishedUrl = "$_appCenterFilesBaseUrl/upload/finished/$packageAssetId?token=$urlEncodedToken"; final response = await http.post( Uri.parse(finishedUrl), headers: _getHeaders(apiToken), @@ -255,8 +251,7 @@ class AppCenterPlugin extends ImpaktfullPlugin { required String ownerName, required Secret apiToken, }) async { - final commitUrl = - "$_appCenterApiBaseUrl/apps/$ownerName/$appName/uploads/releases/$id"; + final commitUrl = "$_appCenterApiBaseUrl/apps/$ownerName/$appName/uploads/releases/$id"; final response = await http.patch( Uri.parse(commitUrl), headers: _getHeaders(apiToken), @@ -276,21 +271,19 @@ class AppCenterPlugin extends ImpaktfullPlugin { required String id, required Secret apiToken, }) async { - final releaseStatusUrl = - "$_appCenterApiBaseUrl/apps/$ownerName/$appName/uploads/releases/$id"; + final releaseStatusUrl = "$_appCenterApiBaseUrl/apps/$ownerName/$appName/uploads/releases/$id"; int? releaseId; var counter = 0; const maxPollAttempts = 15; while (releaseId == null && counter < maxPollAttempts) { - ImpaktfullCliLogger.log('Checking if release is ready...'); + ImpaktfullCliLogger.verbose('Checking if release is ready...'); final pollResult = await http.get( Uri.parse(releaseStatusUrl), headers: _getHeaders(apiToken), ); - final pollResultJson = - json.decode(pollResult.body) as Map; + final pollResultJson = json.decode(pollResult.body) as Map; releaseId = pollResultJson['release_distinct_id'] as int?; counter++; await Future.delayed(Duration(seconds: 3)); @@ -311,15 +304,12 @@ class AppCenterPlugin extends ImpaktfullPlugin { required bool notifyListeners, required Secret apiToken, }) async { - final distributeUrl = - "$_appCenterApiBaseUrl/apps/$ownerName/$appName/releases/$releaseId"; + final distributeUrl = "$_appCenterApiBaseUrl/apps/$ownerName/$appName/releases/$releaseId"; final response = await http.patch( Uri.parse(distributeUrl), headers: _getHeaders(apiToken), body: json.encode({ - 'destinations': distributionGroups - .map((distributionGroup) => {'name': distributionGroup}) - .toList(), + 'destinations': distributionGroups.map((distributionGroup) => {'name': distributionGroup}).toList(), "notify_testers": notifyListeners, }), ); @@ -348,7 +338,6 @@ class AppCenterPlugin extends ImpaktfullPlugin { if (_extensionMimeTypeMapper.containsKey(extension)) { return _extensionMimeTypeMapper[extension]!; } - throw ImpaktfullCliError( - 'Extension `$extension` is not supported to upload to AppCenter using the impaktfull_cli'); + throw ImpaktfullCliError('Extension `$extension` is not supported to upload to AppCenter using the impaktfull_cli'); } } diff --git a/lib/src/integrations/apple_certificate/plugin/mac_os_keychain_plugin.dart b/lib/src/integrations/apple_certificate/plugin/mac_os_keychain_plugin.dart index 546a01b..cef5dfa 100644 --- a/lib/src/integrations/apple_certificate/plugin/mac_os_keychain_plugin.dart +++ b/lib/src/integrations/apple_certificate/plugin/mac_os_keychain_plugin.dart @@ -19,29 +19,14 @@ class MacOsKeyChainPlugin extends ImpaktfullCliPlugin { ) async { final keyChainPath = await _getKeyChainPath(name); if (keyChainPath != null) { - throw ImpaktfullCliError( - '`$name` keychain already exists, make sure to remove it first.'); + throw ImpaktfullCliError('`$name` keychain already exists, make sure to remove it first.'); } final fullKeyChainName = _fullKeyChainName(name); - ImpaktfullCliLogger.debug('Create Apple KeyChain ($fullKeyChainName)'); - await processRunner.runProcess([ - 'security', - 'create-keychain', - '-p', - password.value, - fullKeyChainName - ]); + ImpaktfullCliLogger.verbose('Create Apple KeyChain ($fullKeyChainName)'); + await processRunner.runProcess(['security', 'create-keychain', '-p', password.value, fullKeyChainName]); final keyChain = await _getUserKeyChains(); - await processRunner.runProcess([ - 'security', - 'list-keychains', - '-d', - 'user', - '-s', - fullKeyChainName, - ...keyChain - ]); + await processRunner.runProcess(['security', 'list-keychains', '-d', 'user', '-s', fullKeyChainName, ...keyChain]); } Future unlockKeyChain( @@ -50,14 +35,11 @@ class MacOsKeyChainPlugin extends ImpaktfullCliPlugin { ) async { final keyChainPath = await _getKeyChainPath(name); if (keyChainPath == null) { - throw ImpaktfullCliError( - '`$name` keychain does not exists. In order to unlock a keychain, it should be created first.'); + throw ImpaktfullCliError('`$name` keychain does not exists. In order to unlock a keychain, it should be created first.'); } final fullName = _fullKeyChainName(name); - await processRunner - .runProcess(['security', 'set-keychain-settings', fullName]); - await processRunner.runProcess( - ['security', 'unlock-keychain', '-p', password.value, fullName]); + await processRunner.runProcess(['security', 'set-keychain-settings', fullName]); + await processRunner.runProcess(['security', 'unlock-keychain', '-p', password.value, fullName]); } Future addCertificateToKeyChain( @@ -95,9 +77,8 @@ class MacOsKeyChainPlugin extends ImpaktfullCliPlugin { String name, ) async { final fullKeyChainName = _fullKeyChainName(name); - ImpaktfullCliLogger.debug('Remove Apple KeyChain ($fullKeyChainName)'); - await processRunner - .runProcess(['security', 'delete-keychain', fullKeyChainName]); + ImpaktfullCliLogger.verbose('Remove Apple KeyChain ($fullKeyChainName)'); + await processRunner.runProcess(['security', 'delete-keychain', fullKeyChainName]); } Future setDefaultKeyChain(String name) async { @@ -108,28 +89,21 @@ class MacOsKeyChainPlugin extends ImpaktfullCliPlugin { path = await _getKeyChainPath(name); } if (path == null) { - throw ImpaktfullCliError( - '`$name` keychain does not exists. In order to set the default keychain, it should be created first.'); + throw ImpaktfullCliError('`$name` keychain does not exists. In order to set the default keychain, it should be created first.'); } - ImpaktfullCliLogger.debug('Set default Apple KeyChain ($path)'); - await processRunner - .runProcess(['security', 'default-keychain', '-s', path]); + ImpaktfullCliLogger.verbose('Set default Apple KeyChain ($path)'); + await processRunner.runProcess(['security', 'default-keychain', '-s', path]); } Future getDefaultKeyChain() async { - final keychainsString = - await processRunner.runProcess(['security', 'default-keychain']); + final keychainsString = await processRunner.runProcess(['security', 'default-keychain']); return keychainsString.trim().replaceAll('"', ''); } Future> _getUserKeyChains() async { - final keychainsString = await processRunner - .runProcess(['security', 'list-keychains', '-d', 'user']); - final keychainsList = - keychainsString.split('\n').where((element) => element.isNotEmpty); - return keychainsList - .map((keychain) => keychain.replaceAll('"', '').trim()) - .toList(); + final keychainsString = await processRunner.runProcess(['security', 'list-keychains', '-d', 'user']); + final keychainsList = keychainsString.split('\n').where((element) => element.isNotEmpty); + return keychainsList.map((keychain) => keychain.replaceAll('"', '').trim()).toList(); } Future _getKeyChainPath(String keyChain) async { @@ -148,6 +122,6 @@ class MacOsKeyChainPlugin extends ImpaktfullCliPlugin { for (final keyChain in keyChains) { sb.writeln('\t"$keyChain"'); } - ImpaktfullCliLogger.debug(sb.toString()); + ImpaktfullCliLogger.verbose(sb.toString()); } } diff --git a/lib/src/integrations/ci_cd/plugin/ci_cd_plugin.dart b/lib/src/integrations/ci_cd/plugin/ci_cd_plugin.dart index ed84d88..2e33372 100644 --- a/lib/src/integrations/ci_cd/plugin/ci_cd_plugin.dart +++ b/lib/src/integrations/ci_cd/plugin/ci_cd_plugin.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:impaktfull_cli/src/core/plugin/impaktfull_plugin.dart'; -import 'package:impaktfull_cli/src/core/util/cli/force_quit_util.dart'; +import 'package:impaktfull_cli/src/core/util/input_listener/force_quit_listener.dart'; import 'package:impaktfull_cli/src/integrations/appcenter/plugin/appcenter_plugin.dart'; import 'package:impaktfull_cli/src/integrations/apple_certificate/plugin/mac_os_keychain_plugin.dart'; import 'package:impaktfull_cli/src/integrations/appcenter/model/appcenter_upload_config.dart'; @@ -88,8 +88,9 @@ class CiCdPlugin extends ImpaktfullPlugin { if (playStoreUploadConfig != null) { await playStorePlugin.uploadToPlayStore( file: file, - serviceAccountCredentialsFile: - playStoreUploadConfig.serviceAccountCredentialsFile, + serviceAccountCredentialsFile: playStoreUploadConfig.serviceAccountCredentialsFile, + trackType: playStoreUploadConfig.trackType, + releaseStatus: playStoreUploadConfig.releaseStatus, ); } } @@ -152,8 +153,7 @@ class CiCdPlugin extends ImpaktfullPlugin { await testflightPlugin.uploadToTestflightWithEmailPassword( file: file, email: testflightUploadConfig.credentials?.userName, - appSpecificPassword: - testflightUploadConfig.credentials?.appSpecificPassword, + appSpecificPassword: testflightUploadConfig.credentials?.appSpecificPassword, type: testflightUploadConfig.type, ); } @@ -197,14 +197,12 @@ class CiCdPlugin extends ImpaktfullPlugin { Secret? globalKeyChainPassword, }) async { ImpaktfullCliEnvironment.requiresMacOs(reason: 'Building iOS/macOS apps'); - final globalKeyChainPasswordSecret = globalKeyChainPassword ?? - ImpaktfullCliEnvironmentVariables.getUnlockKeyChainPassword(); + final globalKeyChainPasswordSecret = globalKeyChainPassword ?? ImpaktfullCliEnvironmentVariables.getUnlockKeyChainPassword(); final defaultKeyChain = await macOsKeyChainPlugin.getDefaultKeyChain(); - await macOsKeyChainPlugin.createKeyChain( - keyChainName, globalKeyChainPasswordSecret); + await macOsKeyChainPlugin.createKeyChain(keyChainName, globalKeyChainPasswordSecret); - await ForceQuitUtil.catchForceQuit( + await ForceQuitListener.catchForceQuit( () async { await macOsKeyChainPlugin.setDefaultKeyChain(keyChainName); // await macOsKeyChainPlugin.unlockKeyChain(keyChainName, globalKeyChainPasswordSecret); diff --git a/lib/src/integrations/flutter/build/plugin/flutter_build_plugin.dart b/lib/src/integrations/flutter/build/plugin/flutter_build_plugin.dart index 35135af..5dff136 100644 --- a/lib/src/integrations/flutter/build/plugin/flutter_build_plugin.dart +++ b/lib/src/integrations/flutter/build/plugin/flutter_build_plugin.dart @@ -24,12 +24,13 @@ class FlutterBuildPlugin extends ImpaktfullCliPlugin { String? flavor, String? suffix, }) async { + ImpaktfullCliLogger.setSpinnerPrefix('VersionBump'); + ImpaktfullCliLogger.startSpinner('Validating git clean'); final isGitProject = ImpaktfullCliEnvironment.isInstalled(CliTool.git); if (isGitProject) { final isGitClean = await GitUtil.isGitClean(processRunner); if (!isGitClean) { - throw ImpaktfullCliError( - 'Git is not clean. Please commit or stash your changes before bumping the version.'); + throw ImpaktfullCliError('Git is not clean. Please commit or stash your changes before bumping the version.'); } } final file = File('release_config.json'); @@ -42,6 +43,7 @@ class FlutterBuildPlugin extends ImpaktfullCliPlugin { if (suffix != null) { buildNrKey += '_$suffix'; } + ImpaktfullCliLogger.startSpinner('bumping for `$buildNrKey`'); if (file.existsSync()) { final content = file.readAsStringSync(); final orignalConfigData = jsonDecode(content) as Map; @@ -51,8 +53,7 @@ class FlutterBuildPlugin extends ImpaktfullCliPlugin { } } buildNr++; - ImpaktfullCliLogger.verbose( - 'New build_nr: $buildNr (for key: $buildNrKey)'); + ImpaktfullCliLogger.verbose('New build_nr: $buildNr (for key: $buildNrKey)'); newConfigData[buildNrKey] = buildNr; if (!file.existsSync()) { file.createSync(recursive: true); @@ -72,6 +73,7 @@ class FlutterBuildPlugin extends ImpaktfullCliPlugin { 'Bump build_nr to $buildNr (for key: $buildNrKey)', ]); } + ImpaktfullCliLogger.clearSpinnerPrefix(); return buildNr; } @@ -83,6 +85,8 @@ class FlutterBuildPlugin extends ImpaktfullCliPlugin { String? splitDebugInfoPath = 'build/debug-info', int? buildNr, }) async { + ImpaktfullCliLogger.setSpinnerPrefix('Flutter Build Android'); + ImpaktfullCliLogger.startSpinner('Building `$flavor`'); await processRunner.runProcess([ if (ImpaktfullCliEnvironment.instance.isFvmProject) ...[ 'fvm', @@ -108,12 +112,11 @@ class FlutterBuildPlugin extends ImpaktfullCliPlugin { '--build-number=$buildNr', ], ]); - final file = File(join(extension.getBuildDirectory(flavor: flavor).path, - 'app-$flavor-release.${extension.fileExtension}')); + final file = File(join(extension.getBuildDirectory(flavor: flavor).path, 'app-$flavor-release.${extension.fileExtension}')); if (!file.existsSync()) { - throw ImpaktfullCliError( - 'After building $flavor for Android, `${file.path}` does not exists.'); + throw ImpaktfullCliError('After building $flavor for Android, `${file.path}` does not exists.'); } + ImpaktfullCliLogger.clearSpinnerPrefix(); return file; } @@ -125,6 +128,8 @@ class FlutterBuildPlugin extends ImpaktfullCliPlugin { String? splitDebugInfoPath = '.build/debug-info', int? buildNr, }) async { + ImpaktfullCliLogger.setSpinnerPrefix('Flutter Build iOS'); + ImpaktfullCliLogger.startSpinner('Building `$flavor`'); final buildDirectory = extension.getBuildDirectory(); if (buildDirectory.existsSync()) { buildDirectory.deleteSync(recursive: true); @@ -160,11 +165,9 @@ class FlutterBuildPlugin extends ImpaktfullCliPlugin { ], ]); final files = buildDirectory.listSync(); - final result = files.where((element) => - path.extension(element.path) == '.${extension.fileExtension}'); + final result = files.where((element) => path.extension(element.path) == '.${extension.fileExtension}'); if (result.isEmpty) { - throw ImpaktfullCliError( - 'After building $flavor for iOS, `${buildDirectory.path}` does not contain an `${extension.fileExtension}` file.'); + throw ImpaktfullCliError('After building $flavor for iOS, `${buildDirectory.path}` does not contain an `${extension.fileExtension}` file.'); } if (result.length > 1) { throw ImpaktfullCliError( @@ -173,9 +176,9 @@ class FlutterBuildPlugin extends ImpaktfullCliPlugin { final ipaFile = File(result.first.path); if (!ipaFile.existsSync()) { - throw ImpaktfullCliError( - 'After building $flavor for iOS, `${ipaFile.path}` does not exists.'); + throw ImpaktfullCliError('After building $flavor for iOS, `${ipaFile.path}` does not exists.'); } + ImpaktfullCliLogger.clearSpinnerPrefix(); return ipaFile; } } diff --git a/lib/src/integrations/one_password/plugin/one_password_plugin.dart b/lib/src/integrations/one_password/plugin/one_password_plugin.dart index 0c49203..f417a3e 100644 --- a/lib/src/integrations/one_password/plugin/one_password_plugin.dart +++ b/lib/src/integrations/one_password/plugin/one_password_plugin.dart @@ -7,8 +7,7 @@ import 'package:impaktfull_cli/src/core/util/logger/logger.dart'; import 'package:impaktfull_cli/src/integrations/testflight/model/testflight_credentials.dart'; class OnePasswordPlugin extends ImpaktfullCliPlugin { - String get serviceAccountEnvKey => - ImpaktfullCliEnvironmentVariables.envKeyOnePasswordAccountToken; + String get serviceAccountEnvKey => ImpaktfullCliEnvironmentVariables.envKeyOnePasswordAccountToken; const OnePasswordPlugin({ required super.processRunner, @@ -16,16 +15,24 @@ class OnePasswordPlugin extends ImpaktfullCliPlugin { Future _executeOnePasswordCommand( List args, { + required String log, Secret? rawServiceAccount, - }) async => - processRunner.runProcess( - args, - environment: { - if (rawServiceAccount != null) ...{ - serviceAccountEnvKey: rawServiceAccount.value, - }, + }) async { + ImpaktfullCliLogger.startSpinner( + '1Password: $log', + skipPrefix: true, + ); + final result = await processRunner.runProcess( + args, + environment: { + if (rawServiceAccount != null) ...{ + serviceAccountEnvKey: rawServiceAccount.value, }, - ); + }, + ); + ImpaktfullCliLogger.endSpinner(); + return result; + } Future downloadFile({ required String opUuid, @@ -52,6 +59,7 @@ class OnePasswordPlugin extends ImpaktfullCliPlugin { vaultName, ], ], + log: 'Downloading file from 1Password', rawServiceAccount: rawServiceAccount, ); return exportFile; @@ -97,6 +105,7 @@ class OnePasswordPlugin extends ImpaktfullCliPlugin { 'read', 'op://$vaultName/$opUuid/$fieldName', ], + log: 'Reading field ($fieldName) from 1Password', rawServiceAccount: rawServiceAccount, ); diff --git a/lib/src/integrations/playstore/model/playstore_upload_config.dart b/lib/src/integrations/playstore/model/playstore_upload_config.dart index 4133400..73b966c 100644 --- a/lib/src/integrations/playstore/model/playstore_upload_config.dart +++ b/lib/src/integrations/playstore/model/playstore_upload_config.dart @@ -6,8 +6,32 @@ class PlayStoreUploadConfig { /// 1: file located at `android/playstore_credentials.json` /// 2: env variable value of `GOOGLE_SERVICE_ACCOUNT_JSON_RAW` final File? serviceAccountCredentialsFile; + final PlaystoreTrackType trackType; + final PlaystoreReleaseStatus releaseStatus; const PlayStoreUploadConfig({ this.serviceAccountCredentialsFile, + this.trackType = PlaystoreTrackType.internal, + this.releaseStatus = PlaystoreReleaseStatus.draft, }); } + +enum PlaystoreTrackType { + internal('internal'); + + final String value; + + const PlaystoreTrackType( + this.value, + ); +} + +enum PlaystoreReleaseStatus { + completed('completed'), + draft('draft'); + + final String value; + const PlaystoreReleaseStatus( + this.value, + ); +} diff --git a/lib/src/integrations/playstore/plugin/playstore_plugin.dart b/lib/src/integrations/playstore/plugin/playstore_plugin.dart index 416d7c0..df43bc0 100644 --- a/lib/src/integrations/playstore/plugin/playstore_plugin.dart +++ b/lib/src/integrations/playstore/plugin/playstore_plugin.dart @@ -3,50 +3,105 @@ import 'dart:io'; import 'package:googleapis/androidpublisher/v3.dart'; import "package:googleapis_auth/auth_io.dart"; +import 'package:impaktfull_cli/src/core/cli_constants.dart'; import 'package:impaktfull_cli/src/core/model/data/secret.dart'; import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_error.dart'; import 'package:impaktfull_cli/src/core/plugin/impaktfull_cli_plugin.dart'; import 'package:impaktfull_cli/src/core/util/args/env/impaktfull_cli_environment.dart'; import 'package:impaktfull_cli/src/core/util/args/env/impaktfull_cli_environment_variables.dart'; import 'package:impaktfull_cli/src/core/util/logger/logger.dart'; +import 'package:impaktfull_cli/src/integrations/playstore/model/playstore_upload_config.dart'; import 'package:path/path.dart'; class PlayStorePlugin extends ImpaktfullCliPlugin { - const PlayStorePlugin({ + final _apkOutputDirectory = Directory(join(CliConstants.buildFolderPath, 'aab_to_apk_output')); + PlayStorePlugin({ required super.processRunner, }); Future uploadToPlayStore({ required File file, + required PlaystoreTrackType trackType, + required PlaystoreReleaseStatus releaseStatus, File? serviceAccountCredentialsFile, }) async { + ImpaktfullCliLogger.setSpinnerPrefix('PlayStore upload'); + ImpaktfullCliLogger.startSpinner('Initializing'); if (!file.existsSync()) { throw ImpaktfullCliError('File `${file.path}` does not exists'); } + ImpaktfullCliLogger.startSpinner('Get packageName from file'); final packageName = await _getPackageName(file); + ImpaktfullCliLogger.startSpinner('Get versionCode from file'); + final versionCode = await _getVersionCode(file); + ImpaktfullCliLogger.startSpinner('Get versionName from file'); + final versionName = await _getVersionName(file); + if (_apkOutputDirectory.existsSync()) { + _apkOutputDirectory.deleteSync(recursive: true); + } ImpaktfullCliLogger.verbose('Detected package name: `$packageName`'); - return _runWithGoogleClient( - serviceAccountCredentialsFile: serviceAccountCredentialsFile, - scopes: [ - AndroidPublisherApi.androidpublisherScope, - ], - handler: (client) async { - final api = AndroidPublisherApi(client); + ImpaktfullCliLogger.verbose('Detected version code: `$versionCode`'); + ImpaktfullCliLogger.verbose('Detected version name: `$versionName`'); + try { + await _runWithGoogleClient( + serviceAccountCredentialsFile: serviceAccountCredentialsFile, + scopes: [ + AndroidPublisherApi.androidpublisherScope, + ], + handler: (client) async { + final api = AndroidPublisherApi(client); + + ImpaktfullCliLogger.startSpinner('Create new release'); + final appEdit = await api.edits.insert( + AppEdit(), + packageName, + ); + final appEditId = appEdit.id; + if (appEditId == null) { + throw ImpaktfullCliError('AppEdit ID is null'); + } + ImpaktfullCliLogger.startSpinner('Upload file'); + await api.edits.bundles.upload( + packageName, + appEditId, + uploadMedia: Media(file.openRead(), await file.length()), + uploadOptions: UploadOptions(), + ); + + ImpaktfullCliLogger.startSpinner('Create release track (${trackType.value} - ${releaseStatus.value})'); + final trackRelease = Track( + releases: [ + TrackRelease( + versionCodes: [ + versionCode, + ], + name: '$versionName ($versionCode)', + status: releaseStatus.value, + ), + ], + track: trackType.value, + ); + await api.edits.tracks.update( + trackRelease, + packageName, + appEditId, + trackType.value, + ); - final appEdit = await api.edits.insert(AppEdit(), packageName); - final appEditId = appEdit.id; - if (appEditId == null) { - throw ImpaktfullCliError('AppEdit ID is null'); - } - await api.edits.bundles.upload( - packageName, - appEditId, - uploadMedia: Media(file.openRead(), await file.length()), - uploadOptions: UploadOptions(), - ); - await api.edits.commit(packageName, appEditId); - }, - ); + ImpaktfullCliLogger.startSpinner('Commit release'); + await api.edits.commit( + packageName, + appEditId, + ); + }, + ); + ImpaktfullCliLogger.clearSpinnerPrefix(); + } on DetailedApiRequestError catch (e) { + if (e.message == 'Version code $versionCode has already been used.') { + throw ImpaktfullCliError('The version code must be higher than the previously uploaded version. (must be higher than $versionCode)'); + } + rethrow; + } } // Google Services Implementation @@ -57,10 +112,8 @@ class PlayStorePlugin extends ImpaktfullCliPlugin { }) async { AutoRefreshingAuthClient? client_; try { - final serviceAccountCredentials = - _getServiceAccountCredentials(serviceAccountCredentialsFile); - client_ = - await clientViaServiceAccount(serviceAccountCredentials, scopes); + final serviceAccountCredentials = _getServiceAccountCredentials(serviceAccountCredentialsFile); + client_ = await clientViaServiceAccount(serviceAccountCredentials, scopes); return await handler(client_); } finally { @@ -68,8 +121,8 @@ class PlayStorePlugin extends ImpaktfullCliPlugin { } } - ServiceAccountCredentials _getServiceAccountCredentials( - File? serviceAccountCredentialsFile) { + ServiceAccountCredentials _getServiceAccountCredentials(File? serviceAccountCredentialsFile) { + ImpaktfullCliLogger.startSpinner('Assembling google service account'); var file = serviceAccountCredentialsFile; if (file == null) { final fallbackFile = File(join( @@ -85,44 +138,64 @@ class PlayStorePlugin extends ImpaktfullCliPlugin { if (file != null && file.existsSync()) { credentials = Secret(file.readAsStringSync()); } else { - credentials = ImpaktfullCliEnvironmentVariables - .getGoogleServiceAccountCredentials(); + credentials = ImpaktfullCliEnvironmentVariables.getGoogleServiceAccountCredentials(); } final serviceAccountCredentialsJson = jsonDecode(credentials.value); return ServiceAccountCredentials.fromJson(serviceAccountCredentialsJson); } Future _getPackageName(File file) async { + final apkFile = await _getApk(file); + final config = await processRunner.runProcess(['aapt2', 'dump', 'badging', apkFile.path]); + const regex = r"package: name='([^']*)'"; + final value = RegExp(regex).firstMatch(config)?.group(1); + if (value == null) { + throw ImpaktfullCliError('Package name not found'); + } + return value; + } + + Future _getVersionCode(File file) async { + final apkFile = await _getApk(file); + final config = await processRunner.runProcess(['aapt2', 'dump', 'badging', apkFile.path]); + const regex = r"versionCode='(\d+)'"; + final value = RegExp(regex).firstMatch(config)?.group(1); + if (value == null) { + throw ImpaktfullCliError('Version code not found'); + } + return value; + } + + Future _getVersionName(File file) async { + final apkFile = await _getApk(file); + final config = await processRunner.runProcess(['aapt2', 'dump', 'badging', apkFile.path]); + const regex = r"versionName='([^']*)'"; + final value = RegExp(regex).firstMatch(config)?.group(1); + if (value == null) { + throw ImpaktfullCliError('Version name not found'); + } + return value; + } + + Future _getApk(File file) async { final fileExtension = extension(file.path); if (fileExtension == '.aab') { final apksFile = File('app.apks'); final apksZipFile = File('aab_to_apks.zip'); - final apkOutputDirectory = Directory(join('tmp', 'aab_to_apk_output')); - final baseApkFile = - File(join(apkOutputDirectory.path, 'splits', 'base-master.apk')); - await processRunner.runProcess([ - 'bundletool', - 'build-apks', - '--bundle=${file.path}', - '--output=${apksFile.path}' - ]); + final baseApkFile = File(join(_apkOutputDirectory.path, 'splits', 'base-master.apk')); + await processRunner.runProcess(['bundletool', 'build-apks', '--bundle=${file.path}', '--output=${apksFile.path}']); apksFile.renameSync(apksZipFile.path); - if (apkOutputDirectory.existsSync()) { - apkOutputDirectory.deleteSync(recursive: true); + if (_apkOutputDirectory.existsSync()) { + _apkOutputDirectory.deleteSync(recursive: true); } - apkOutputDirectory.createSync(recursive: true); - await processRunner.runProcess( - ['unzip', apksZipFile.path, '-d', apkOutputDirectory.path]); + _apkOutputDirectory.createSync(recursive: true); + await processRunner.runProcess(['unzip', apksZipFile.path, '-d', _apkOutputDirectory.path]); apksZipFile.deleteSync(recursive: true); - final packageName = await _getPackageName(baseApkFile); - apkOutputDirectory.deleteSync(recursive: true); - return packageName; + return _getApk(baseApkFile); } else if (fileExtension == '.apk') { - return processRunner - .runProcess(['aapt2', 'dump', 'packagename', file.path]); + return file; } else { - throw ImpaktfullCliError( - 'Automatic detection of the package name is currently only supported for [.aab & .apk] files'); + throw ImpaktfullCliError('Automatic detection of the package name is currently only supported for [.aab & .apk] files'); } } } diff --git a/lib/src/integrations/testflight/plugin/testflight_plugin.dart b/lib/src/integrations/testflight/plugin/testflight_plugin.dart index 834f3f7..80ebdde 100644 --- a/lib/src/integrations/testflight/plugin/testflight_plugin.dart +++ b/lib/src/integrations/testflight/plugin/testflight_plugin.dart @@ -6,6 +6,7 @@ import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_error.dart'; import 'package:impaktfull_cli/src/core/plugin/impaktfull_cli_plugin.dart'; import 'package:impaktfull_cli/src/core/util/args/env/impaktfull_cli_environment.dart'; import 'package:impaktfull_cli/src/core/util/args/env/impaktfull_cli_environment_variables.dart'; +import 'package:impaktfull_cli/src/core/util/logger/logger.dart'; import 'package:path/path.dart'; class TestFlightPlugin extends ImpaktfullCliPlugin { @@ -19,27 +20,25 @@ class TestFlightPlugin extends ImpaktfullCliPlugin { Secret? appSpecificPassword, String type = 'ios', }) async { + ImpaktfullCliLogger.setSpinnerPrefix('Testflight upload'); + ImpaktfullCliLogger.startSpinner('Initializing'); if (!file.existsSync()) { throw ImpaktfullCliError('File `${file.path}` does not exists'); } - ImpaktfullCliEnvironment.requiresMacOs( - reason: 'Uploading to testflight can only be done on macos'); + ImpaktfullCliEnvironment.requiresMacOs(reason: 'Uploading to testflight can only be done on macos'); ImpaktfullCliEnvironment.requiresInstalledTools([CliTool.xcodeSelect]); - final path = - await processRunner.runProcess(['xcode-select', '--print-path']); + final path = await processRunner.runProcess(['xcode-select', '--print-path']); final xCodeDirectory = Directory(path); - final xCodeToolsDirectory = - Directory(join(xCodeDirectory.path, 'usr', 'bin')); + final xCodeToolsDirectory = Directory(join(xCodeDirectory.path, 'usr', 'bin')); final aToolFile = File(join(xCodeToolsDirectory.path, 'altool')); if (!aToolFile.existsSync()) { - throw ImpaktfullCliError( - '`${aToolFile.path}` does not exists, `altool` is required to upload to testflight'); + throw ImpaktfullCliError('`${aToolFile.path}` does not exists, `altool` is required to upload to testflight'); } email = email ?? ImpaktfullCliEnvironmentVariables.getAppleEmail(); - appSpecificPassword = appSpecificPassword ?? - ImpaktfullCliEnvironmentVariables.getAppleAppSpecificPassword(); + appSpecificPassword = appSpecificPassword ?? ImpaktfullCliEnvironmentVariables.getAppleAppSpecificPassword(); + ImpaktfullCliLogger.startSpinner('Uploading'); final result = await processRunner.runProcess([ aToolFile.path, '--upload-app', @@ -56,12 +55,10 @@ class TestFlightPlugin extends ImpaktfullCliPlugin { throw ImpaktfullCliError( 'Sign in with the app-specific password you generated. If you forgot the app-specific password or need to create a new one, go to appleid.apple.com'); } else if (result.contains('The auth server returned a bad status code.')) { - throw ImpaktfullCliError( - 'Error during authentication with appstoreconnect (check email, app-specific password, connection to the internet)'); - } else if (result.contains('ContentDelivery Code=-19232') || - result.contains('ContentDelivery Code=90062')) { - throw ImpaktfullCliError( - 'The bundle version must be higher than the previously uploaded version'); + throw ImpaktfullCliError('Error during authentication with appstoreconnect (check email, app-specific password, connection to the internet)'); + } else if (result.contains('ContentDelivery Code=-19232') || result.contains('ContentDelivery Code=90062')) { + throw ImpaktfullCliError('The bundle version must be higher than the previously uploaded version'); } + ImpaktfullCliLogger.clearSpinnerPrefix(); } } diff --git a/pubspec.yaml b/pubspec.yaml index a80f96e..ef0cc02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,7 @@ environment: dependencies: args: ^2.4.2 code_builder: ^4.10.0 + cli_spin: ^1.0.1 http: ^1.1.0 meta: ^1.1.0 path: ^1.8.3