feat(shorebird_cli): shorebird channels list (#201)

This commit is contained in:
Felix Angelov
2023-03-29 15:10:33 -05:00
committed by GitHub
parent 5faf079149
commit 3dd95458e8
8 changed files with 275 additions and 9 deletions
+33 -9
View File
@@ -239,6 +239,29 @@ Build a new release of your application using the `shorebird build` command:
shorebird build
```
### List Channels
See all available channels for your application using the `shorebird channels list` command:
```bash
shorebird channels list
```
**Sample**
```
shorebird channels list
📱 App ID: 61fc9c16-3c4a-4825-a155-9765993614aa
📺 Channels
┌─────────────┐
│ Name │
├─────────────┤
│ stable │
├─────────────┤
│ development │
└─────────────┘
```
### Usage
```
@@ -252,15 +275,16 @@ Global options:
--[no-]verbose Noisy logging, including all shell commands executed.
Available commands:
apps Manage your Shorebird apps.
build Build a new release of your application.
init Initialize Shorebird.
login Login as a new Shorebird user.
logout Logout of the current Shorebird user
patch Publish new patches for a specific release to Shorebird.
release Builds and submits your app to Shorebird.
run Run the Flutter application.
upgrade Upgrade your copy of Shorebird.
apps Manage your Shorebird apps.
build Build a new release of your application.
channels Manage the channels for your Shorebird app.
init Initialize Shorebird.
login Login as a new Shorebird user.
logout Logout of the current Shorebird user
patch Publish new patches for a specific release to Shorebird.
release Builds and submits your app to Shorebird.
run Run the Flutter application.
upgrade Upgrade your copy of Shorebird.
Run "shorebird help <command>" for more information about a command.
```
@@ -48,6 +48,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
addCommand(AppsCommand(logger: _logger));
addCommand(BuildCommand(logger: _logger));
addCommand(ChannelsCommand(logger: _logger));
addCommand(InitCommand(logger: _logger));
addCommand(LoginCommand(logger: _logger));
addCommand(LogoutCommand(logger: _logger));
@@ -0,0 +1,2 @@
export 'channels_command.dart';
export 'list_channels_command.dart';
@@ -0,0 +1,19 @@
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/commands/commands.dart';
/// {@template channels_command}
/// `shorebird channels`
/// Manage the channels for your Shorebird app.
/// {@endtemplate}
class ChannelsCommand extends ShorebirdCommand {
/// {@macro channels_command}
ChannelsCommand({required super.logger}) {
addSubcommand(ListChannelsCommand(logger: logger));
}
@override
String get description => 'Manage the channels for your Shorebird app.';
@override
String get name => 'channels';
}
@@ -0,0 +1,110 @@
import 'dart:async';
import 'package:barbecue/barbecue.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
/// {@template list_channels_command}
/// `shorebird channels list`
/// List all channels for a Shorebird app.
/// {@endtemplate}
class ListChannelsCommand extends ShorebirdCommand with ShorebirdConfigMixin {
/// {@macro list_channels_command}
ListChannelsCommand({
required super.logger,
super.buildCodePushClient,
super.auth,
}) {
argParser.addOption(
_appIdOption,
help: 'The app id to list channels for.',
);
}
static const String _appIdOption = 'app-id';
@override
String get description => 'List all channels for a Shorebird app.';
@override
String get name => 'list';
@override
List<String> get aliases => ['ls'];
@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
logger.err('You must be logged in to view channels.');
return ExitCode.noUser.code;
}
final client = buildCodePushClient(
apiKey: session.apiKey,
hostedUri: hostedUri,
);
final appId = results[_appIdOption] as String? ?? getShorebirdYaml()?.appId;
if (appId == null) {
logger.err(
'''
Could not find an app id.
You must either specify an app id via the "--$_appIdOption" flag or run this command from within a directory with a valid "shorebird.yaml" file.''',
);
return ExitCode.usage.code;
}
final List<Channel> channels;
try {
channels = await client.getChannels(appId: appId);
} catch (error) {
logger.err('$error');
return ExitCode.software.code;
}
logger.info(
'''
📱 App ID: ${lightCyan.wrap(appId)}
📺 Channels''',
);
if (channels.isEmpty) {
logger.info('(empty)');
return ExitCode.success.code;
}
logger.info(channels.prettyPrint());
return ExitCode.success.code;
}
}
extension on List<Channel> {
String prettyPrint() {
const cellStyle = CellStyle(
paddingLeft: 1,
paddingRight: 1,
borderBottom: true,
borderTop: true,
borderLeft: true,
borderRight: true,
);
return Table(
cellStyle: cellStyle,
header: const TableSection(
rows: [
Row(cells: [Cell('Name')])
],
),
body: TableSection(
rows: [
for (final channel in this) Row(cells: [Cell(channel.name)]),
],
),
).render();
}
}
@@ -1,5 +1,6 @@
export 'apps/apps.dart';
export 'build_command.dart';
export 'channels/channels.dart';
export 'init_command.dart';
export 'login_command.dart';
export 'logout_command.dart';
+1
View File
@@ -11,6 +11,7 @@ environment:
dependencies:
archive: ^3.3.6
args: ^2.3.1
barbecue: ^0.4.0
checked_yaml: ^2.0.2
cli_completion: ^0.3.0
cli_util: ^0.4.0
@@ -0,0 +1,108 @@
import 'package:args/args.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/auth/session.dart';
import 'package:shorebird_cli/src/commands/commands.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
import 'package:test/test.dart';
class _MockArgResults extends Mock implements ArgResults {}
class _MockAuth extends Mock implements Auth {}
class _MockCodePushClient extends Mock implements CodePushClient {}
class _MockLogger extends Mock implements Logger {}
void main() {
group('list', () {
const session = Session(apiKey: 'test-api-key');
const appId = 'test-app-id';
late ArgResults argResults;
late Auth auth;
late CodePushClient codePushClient;
late Logger logger;
late ListChannelsCommand command;
setUp(() {
argResults = _MockArgResults();
auth = _MockAuth();
codePushClient = _MockCodePushClient();
logger = _MockLogger();
command = ListChannelsCommand(
auth: auth,
buildCodePushClient: ({required String apiKey, Uri? hostedUri}) {
return codePushClient;
},
logger: logger,
)..testArgResults = argResults;
when(() => argResults['app-id']).thenReturn(appId);
when(() => auth.currentSession).thenReturn(session);
});
test('description is correct', () {
expect(
command.description,
equals('List all channels for a Shorebird app.'),
);
});
test('returns ExitCode.noUser when not logged in', () async {
when(() => auth.currentSession).thenReturn(null);
expect(await command.run(), ExitCode.noUser.code);
});
test('returns ExitCode.usage when app id is missing.', () async {
when(() => argResults['app-id']).thenReturn(null);
expect(await command.run(), ExitCode.usage.code);
});
test('returns ExitCode.software when unable to get channels', () async {
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenThrow(Exception());
expect(await command.run(), ExitCode.software.code);
});
test('returns ExitCode.success when channels are empty', () async {
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenAnswer((_) async => []);
expect(await command.run(), ExitCode.success.code);
verify(() => logger.info('(empty)')).called(1);
});
test('returns ExitCode.success when channels are not empty', () async {
final channels = [
const Channel(id: 0, appId: appId, name: 'stable'),
const Channel(id: 1, appId: appId, name: 'development'),
];
when(
() => codePushClient.getChannels(appId: any(named: 'appId')),
).thenAnswer((_) async => channels);
expect(await command.run(), ExitCode.success.code);
verify(
() => logger.info(
'''
📱 App ID: ${lightCyan.wrap(appId)}
📺 Channels''',
),
).called(1);
verify(
() => logger.info(
'''
┌─────────────┐
│ Name │
├─────────────┤
│ stable │
├─────────────┤
│ development │
└─────────────┘''',
),
).called(1);
});
});
}