From aa36f92dccdc0cb892f104a5f85360098e8ccd4b Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Thu, 26 Feb 2026 16:27:54 -0800 Subject: [PATCH] feat: add --confirm flag to release and patch commands (#3628) --- .../lib/src/commands/patch/patch_command.dart | 17 ++++ .../src/commands/release/release_command.dart | 17 ++++ .../commands/patch/patch_command_test.dart | 88 +++++++++++++++++++ .../release/release_command_test.dart | 63 +++++++++++++ 4 files changed, 185 insertions(+) diff --git a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart index f7668a0f..0ad55cbe 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart @@ -108,6 +108,13 @@ To target the latest release (e.g. the release that was most recently updated) u help: CommonArguments.noConfirmArg.description, negatable: false, ) + // Added for https://github.com/shorebirdtech/shorebird/issues/3223. + // Can be removed fall 2026 or later. + ..addFlag( + 'confirm', + negatable: false, + hide: true, + ) ..addOption( CommonArguments.exportOptionsPlistArg.name, help: CommonArguments.exportOptionsPlistArg.description, @@ -188,6 +195,9 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl /// The target script, if provided. late String? target = results.findOption('target', argParser: argParser); + /// Whether to prompt for confirmation before creating the patch. + bool get confirm => results['confirm'] == true; + /// Whether to allow changes in assets (--allow-asset-diffs). bool get allowAssetDiffs => results['allow-asset-diffs'] == true; @@ -608,6 +618,13 @@ ${styleBold.wrap(lightGreen.wrap('🚀 Ready to publish a new patch!'))} ${summary.join('\n')} '''); + + if (confirm && shorebirdEnv.canAcceptUserInput) { + if (!logger.confirm('Would you like to continue?', defaultValue: true)) { + logger.info('Aborting.'); + throw ProcessExit(ExitCode.success.code); + } + } } /// Downloads the given [releaseArtifact]. diff --git a/packages/shorebird_cli/lib/src/commands/release/release_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_command.dart index ed855a75..dff4b675 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_command.dart @@ -129,6 +129,13 @@ Defaults to "latest" which builds using the latest stable Flutter version.''', help: CommonArguments.noConfirmArg.description, negatable: false, ) + // Added for https://github.com/shorebirdtech/shorebird/issues/3223. + // Can be removed fall 2026 or later. + ..addFlag( + 'confirm', + negatable: false, + hide: true, + ) ..addOption( 'release-version', help: ''' @@ -225,6 +232,9 @@ of the iOS app that is using this module. (aar and ios-framework only)''', } } + /// Whether to prompt for confirmation before creating the release. + bool get confirm => results['confirm'] == true; + /// The shorebird app ID for the current project. String get appId => shorebirdEnv.getShorebirdYaml()!.getAppId(flavor: flavor); @@ -524,6 +534,13 @@ ${styleBold.wrap(lightGreen.wrap('🚀 Ready to create a new release!'))} ${summary.join('\n')} '''); + + if (confirm && shorebirdEnv.canAcceptUserInput) { + if (!logger.confirm('Would you like to continue?', defaultValue: true)) { + logger.info('Aborting.'); + throw ProcessExit(ExitCode.success.code); + } + } } /// Fetches the release with version [version] from the server or creates a diff --git a/packages/shorebird_cli/test/src/commands/patch/patch_command_test.dart b/packages/shorebird_cli/test/src/commands/patch/patch_command_test.dart index 0a5a955d..cb6c4c86 100644 --- a/packages/shorebird_cli/test/src/commands/patch/patch_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/patch_command_test.dart @@ -815,6 +815,94 @@ void main() { }); }); }); + + group('when --confirm is passed', () { + setUp(() { + when(() => argResults['confirm']).thenReturn(true); + when(() => shorebirdEnv.canAcceptUserInput).thenReturn(true); + }); + + group('when user confirms', () { + setUp(() { + when( + () => logger.confirm( + any(), + defaultValue: any(named: 'defaultValue'), + ), + ).thenReturn(true); + }); + + test('continues', () async { + await expectLater( + runWithOverrides( + () => command.logPatchSummary( + app: appMetadata, + releaseVersion: releaseVersion, + patcher: patcher, + patchArtifactBundles: patchArtifactBundles, + ), + ), + completes, + ); + verify( + () => logger.confirm( + 'Would you like to continue?', + defaultValue: true, + ), + ).called(1); + }); + }); + + group('when user declines', () { + setUp(() { + when( + () => logger.confirm( + any(), + defaultValue: any(named: 'defaultValue'), + ), + ).thenReturn(false); + }); + + test('exits with success and prints Aborting.', () async { + await expectLater( + runWithOverrides( + () => command.logPatchSummary( + app: appMetadata, + releaseVersion: releaseVersion, + patcher: patcher, + patchArtifactBundles: patchArtifactBundles, + ), + ), + exitsWithCode(ExitCode.success), + ); + verify(() => logger.info('Aborting.')).called(1); + }); + }); + }); + + group('when --confirm is not passed', () { + setUp(() { + when(() => argResults['confirm']).thenReturn(false); + }); + + test('does not prompt for confirmation', () async { + await expectLater( + runWithOverrides( + () => command.logPatchSummary( + app: appMetadata, + releaseVersion: releaseVersion, + patcher: patcher, + patchArtifactBundles: patchArtifactBundles, + ), + ), + completes, + ); + verifyNever( + () => + logger.confirm(any(), defaultValue: any(named: 'defaultValue')), + ); + }); + }); }); group('when flutter install fails', () { diff --git a/packages/shorebird_cli/test/src/commands/release/release_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_command_test.dart index 10f16789..ea7cb4ca 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_command_test.dart @@ -414,6 +414,69 @@ void main() { }); }); + group('when --confirm is passed', () { + setUp(() { + when(() => argResults['confirm']).thenReturn(true); + when(() => shorebirdEnv.canAcceptUserInput).thenReturn(true); + }); + + group('when user confirms', () { + setUp(() { + when( + () => logger.confirm( + any(), + defaultValue: any(named: 'defaultValue'), + ), + ).thenReturn(true); + }); + + test('continues', () async { + await runWithOverrides(command.run); + verify( + () => logger.confirm( + 'Would you like to continue?', + defaultValue: true, + ), + ).called(1); + }); + }); + + group('when user declines', () { + setUp(() { + when( + () => logger.confirm( + any(), + defaultValue: any(named: 'defaultValue'), + ), + ).thenReturn(false); + }); + + test('exits with success and prints Aborting.', () async { + await expectLater( + runWithOverrides(command.run), + exitsWithCode(ExitCode.success), + ); + verify(() => logger.info('Aborting.')).called(1); + }); + }); + }); + + group('when --confirm is not passed', () { + setUp(() { + when(() => argResults['confirm']).thenReturn(false); + }); + + test('does not prompt for confirmation', () async { + await runWithOverrides(command.run); + verifyNever( + () => logger.confirm( + any(), + defaultValue: any(named: 'defaultValue'), + ), + ); + }); + }); + group('when flavor and target are provided', () { const flavor = 'test-flavor'; const target = 'test-target';