Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create List Command #2367

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/shorebird_cli/lib/src/commands/commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export 'patch/patch.dart';
export 'patches/patches.dart';
export 'preview_command.dart';
export 'release/release.dart';
export 'releases/releases_command.dart';
export 'run_command.dart';
export 'upgrade_command.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'package:collection/collection.dart';
import 'package:intl/intl.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/commands/preview_command.dart';
import 'package:shorebird_cli/src/config/config.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/shorebird_command.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';

/// {@template list_releases_command}
/// A command to list available releases.
/// {@endtemplate}
class ListReleasesCommand extends ShorebirdCommand {
/// {@macro list_releases_command}
ListReleasesCommand() {
argParser
..addOption(
'flavor',
help: 'The flavor for which to list releases.',
)
..addOption(
'limit',
help: 'Limit number of releases to be printed.',
defaultsTo: '$defaultLimit',
);
}

/// The default limit for the number of releases to be printed.
static const int defaultLimit = 10;

@override
String get description => 'List available releases.';

@override
String get name => 'list';

/// The shorebird app ID for the current project.
String get appId => shorebirdEnv.getShorebirdYaml()!.getAppId(flavor: flavor);

/// The build flavor, if provided.
late String? flavor = results['flavor'] as String?;

/// Whether to only show the latest release for each platform.
int get limit {
final arg = int.tryParse(
results['limit'] as String? ?? '$defaultLimit',
);

if (arg == null) {
return defaultLimit;
}

if (arg <= 0) {
return defaultLimit;
}

return arg;
}

final _dateFormat = DateFormat('MM/dd/yyyy h:mm a');

void _logKeyValue(String key, String value) {
logger.info(' ${darkGray.wrap(key)}: $value');
}

void _logRelease(Release release) {
logger.info(release.version);
_logKeyValue('Created', _dateFormat.format(release.createdAt));
_logKeyValue('Last Updated', _dateFormat.format(release.updatedAt));
if (release.activePlatforms case final platforms
when platforms.isNotEmpty) {
final sorted = platforms.map((e) => e.displayName).sorted();
_logKeyValue('Platforms', sorted.join(', '));
}
}

@override
Future<int> run() async {
final releases = await codePushClientWrapper.getReleases(appId: appId);

if (releases.isEmpty) {
logger.info('No releases found for $appId');
return 0;
}

final toDisplay = releases.take(limit).toList();

logger
..info('Found ${releases.length} releases for $appId')
..info('Latest Releases (${toDisplay.length}):')
..info('');

/// Show the most recent last
for (final release in toDisplay.reversed) {
_logRelease(release);
}

return ExitCode.success.code;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:shorebird_cli/src/commands/releases/list_releases_command.dart';
import 'package:shorebird_cli/src/shorebird_command.dart';

/// {@template releases_command}
/// Commands for Shorebird releases.
/// {@endtemplate}
class ReleasesCommand extends ShorebirdCommand {
/// {@macro releases_command}
ReleasesCommand() {
addSubcommand(ListReleasesCommand());
}

@override
String get description => 'Commands for Shorebird releases.';

@override
String get name => 'releases';
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
addCommand(PatchesCommand());
addCommand(PreviewCommand());
addCommand(ReleaseCommand());
addCommand(ReleasesCommand());
addCommand(RunCommand());
addCommand(UpgradeCommand());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import 'dart:io';

import 'package:args/args.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/commands/releases/list_releases_command.dart';
import 'package:shorebird_cli/src/config/config.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
import 'package:test/test.dart';

import '../../mocks.dart';

void main() {
group(ListReleasesCommand, () {
const appId = 'test-app-id';
const flutterRevision = '83305b5088e6fe327fb3334a73ff190828d85713';
const flutterVersion = '3.22.0';
const releaseVersion = '1.2.3+1';
const shorebirdYaml = ShorebirdYaml(appId: appId);
final release = Release(
id: 0,
appId: appId,
version: releaseVersion,
flutterRevision: flutterRevision,
flutterVersion: flutterVersion,
displayName: '1.2.3+1',
platformStatuses: {},
createdAt: DateTime(2023),
updatedAt: DateTime(2023),
);

late ArgResults argResults;
late CodePushClientWrapper codePushClientWrapper;
late ShorebirdLogger logger;
late Progress progress;
late ShorebirdEnv shorebirdEnv;

late ListReleasesCommand command;

R runWithOverrides<R>(R Function() body) {
return runScoped(
body,
values: {
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
loggerRef.overrideWith(() => logger),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
},
);
}

setUpAll(() {
registerFallbackValue(Directory(''));
registerFallbackValue(release);
registerFallbackValue(ReleasePlatform.android);
registerFallbackValue(ReleaseStatus.draft);
registerFallbackValue(ArgParser());
});

setUp(() {
argResults = MockArgResults();
codePushClientWrapper = MockCodePushClientWrapper();
logger = MockShorebirdLogger();
progress = MockProgress();
shorebirdEnv = MockShorebirdEnv();

command = ListReleasesCommand()..testArgResults = argResults;

when(() => logger.progress(any())).thenReturn(progress);
when(() => logger.confirm(any())).thenReturn(true);
when(() => argResults.wasParsed(any())).thenReturn(false);
});

test('has non-empty description', () {
expect(command.description, isNotEmpty);
});

test('default limit is 10', () {
expect(ListReleasesCommand.defaultLimit, 10);
});

group('#run', () {
void verifyReleaseLogs(
Logger logger, {
required int total,
required int actual,
int callCount = 1,
}) {
verify(() => logger.info('Found $total releases for $appId')).called(1);
verify(() => logger.info('Latest Releases ($actual):')).called(1);
verify(() => logger.info('')).called(1);
verify(() => logger.info('1.2.3+1')).called(callCount);
verify(() => logger.info(' Created: 01/01/2023 12:00 AM'))
.called(callCount);
verify(() => logger.info(' Last Updated: 01/01/2023 12:00 AM'))
.called(callCount);
}

test('logs no releases found', () async {
when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml);
when(() => codePushClientWrapper.getReleases(appId: appId))
.thenAnswer((_) => Future.value([]));

await runWithOverrides(() => command.run());

verify(() => logger.info('No releases found for $appId')).called(1);
verifyNoMoreInteractions(logger);
});

test('logs releases', () async {
when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml);
when(() => codePushClientWrapper.getReleases(appId: appId))
.thenAnswer((_) => Future.value([release]));

await runWithOverrides(() => command.run());

verifyReleaseLogs(logger, total: 1, actual: 1);

verifyNoMoreInteractions(logger);
});

test('logs releases with limit', () async {
when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml);
when(() => codePushClientWrapper.getReleases(appId: appId))
.thenAnswer((_) => Future.value([release, release, release]));

when(() => argResults['limit']).thenReturn('1');

await runWithOverrides(() => command.run());

verifyReleaseLogs(logger, total: 3, actual: 1);

verifyNoMoreInteractions(logger);
});

test('logs releases when limit is greater than releases length',
() async {
when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml);
when(() => codePushClientWrapper.getReleases(appId: appId))
.thenAnswer((_) => Future.value([release, release, release]));

when(() => argResults['limit']).thenReturn('10');

await runWithOverrides(() => command.run());

verifyReleaseLogs(logger, total: 3, actual: 3, callCount: 3);

verifyNoMoreInteractions(logger);
});

test('logs default limit when limit is <= 0', () async {
when(() => shorebirdEnv.getShorebirdYaml()).thenReturn(shorebirdYaml);
when(() => codePushClientWrapper.getReleases(appId: appId))
.thenAnswer((_) => Future.value([release, release, release]));

when(() => argResults['limit']).thenReturn('0');

await runWithOverrides(() => command.run());

verifyReleaseLogs(logger, total: 3, actual: 3, callCount: 3);

verifyNoMoreInteractions(logger);
});
});
});
}