feat: Add simple shorebird doctor command that checks for updates (#213)

Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
Bryan Oltman
2023-03-31 16:30:43 -04:00
committed by GitHub
parent 92da9d0ddd
commit f1ff11dfaf
8 changed files with 256 additions and 72 deletions
+20
View File
@@ -78,6 +78,25 @@ shorebird logout
✓ Logging out of shorebird.dev (1ms)
```
### Doctor
To check your environment for common issues, use the `shorebird doctor` command.
```bash
shorebird doctor
```
**Sample**
```
$ shorebird doctor
Doctor summary
Shorebird v0.0.3
No issues detected!
```
### Create App
To create an app use the `shorebird apps create` command. An app id can be specified as a CLI option but shorebird will default to the `app_id` defined in the `shorebird.yaml`
@@ -308,6 +327,7 @@ Available commands:
apps Manage your Shorebird apps.
build Build a new release of your application.
channels Manage the channels for your Shorebird app.
doctor Show information about the installed tooling.
init Initialize Shorebird.
login Login as a new Shorebird user.
logout Logout of the current Shorebird user
@@ -49,6 +49,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
addCommand(AppsCommand(logger: _logger));
addCommand(BuildCommand(logger: _logger));
addCommand(ChannelsCommand(logger: _logger));
addCommand(DoctorCommand(logger: _logger));
addCommand(InitCommand(logger: _logger));
addCommand(LoginCommand(logger: _logger));
addCommand(LogoutCommand(logger: _logger));
@@ -1,6 +1,7 @@
export 'apps/apps.dart';
export 'build_command.dart';
export 'channels/channels.dart';
export 'doctor_command.dart';
export 'init_command.dart';
export 'login_command.dart';
export 'logout_command.dart';
@@ -0,0 +1,55 @@
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/shorebird_version_mixin.dart';
import 'package:shorebird_cli/src/version.dart';
/// {@template doctor_command}
///
/// `shorebird doctor`
/// A command that checks for potential issues with the current shorebird
/// environment.
/// {@endtemplate}
class DoctorCommand extends ShorebirdCommand with ShorebirdVersionMixin {
/// {@macro doctor_command}
DoctorCommand({required super.logger, super.runProcess});
@override
String get name => 'doctor';
@override
String get description => 'Show information about the installed tooling.';
@override
Future<int> run() async {
var numIssues = 0;
final workingDirectory = p.dirname(Platform.script.toFilePath());
logger.info('''
Doctor summary
Shorebird v$packageVersion
''');
final isShorebirdUpToDate = await isShorebirdVersionCurrent(
workingDirectory: workingDirectory,
);
if (!isShorebirdUpToDate) {
numIssues += 1;
logger.info('''
A new version of shorebird is available!
Run `shorebird upgrade` to upgrade.
''');
}
if (numIssues == 0) {
logger.info('No issues detected!');
} else {
logger.info('$numIssues issue${numIssues == 1 ? '' : 's'} detected.');
}
return ExitCode.success.code;
}
}
@@ -3,12 +3,13 @@ import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/shorebird_version_mixin.dart';
/// {@template upgrade_command}
/// `shorebird upgrade`
/// A command which upgrades your copy of Shorebird.
/// {@endtemplate}
class UpgradeCommand extends ShorebirdCommand {
class UpgradeCommand extends ShorebirdCommand with ShorebirdVersionMixin {
/// {@macro upgrade_command}
UpgradeCommand({required super.logger, super.runProcess});
@@ -27,7 +28,7 @@ class UpgradeCommand extends ShorebirdCommand {
late final String currentVersion;
try {
currentVersion = await fetchCurrentVersion(
currentVersion = await fetchCurrentGitHash(
workingDirectory: workingDirectory,
);
} on ProcessException catch (error) {
@@ -38,7 +39,7 @@ class UpgradeCommand extends ShorebirdCommand {
late final String latestVersion;
try {
latestVersion = await fetchLatestVersion(
latestVersion = await fetchLatestGitHash(
workingDirectory: workingDirectory,
);
} on ProcessException catch (error) {
@@ -72,73 +73,4 @@ class UpgradeCommand extends ShorebirdCommand {
return ExitCode.success.code;
}
/// Returns the remote HEAD shorebird version.
///
/// Exits if HEAD isn't pointing to a branch, or there is no upstream.
Future<String> fetchLatestVersion({required String workingDirectory}) async {
// Fetch upstream branch's commits and tags
await runProcess(
'git',
['fetch', '--tags'],
workingDirectory: workingDirectory,
);
// Get the latest commit revision of the upstream
return _gitRevParse('@{upstream}', workingDirectory: workingDirectory);
}
/// Returns the local HEAD shorebird version.
///
/// Exits if HEAD isn't pointing to a branch, or there is no upstream.
Future<String> fetchCurrentVersion({
required String workingDirectory,
}) async {
// Get the commit revision of HEAD
return _gitRevParse('HEAD', workingDirectory: workingDirectory);
}
Future<String> _gitRevParse(
String revision, {
String? workingDirectory,
}) async {
// Get the commit revision of HEAD
final result = await runProcess(
'git',
['rev-parse', '--verify', revision],
workingDirectory: workingDirectory,
);
if (result.exitCode != 0) {
throw ProcessException(
'git',
['rev-parse', '--verify', revision],
'${result.stderr}',
result.exitCode,
);
}
return '${result.stdout}'.trim();
}
/// Attempts a hard reset to the given revision.
///
/// This is a reset instead of fast forward because if we are on a release
/// branch with cherry picks, there may not be a direct fast-forward route
/// to the next release.
Future<void> attemptReset({
required String newRevision,
required String workingDirectory,
}) async {
final result = await runProcess(
'git',
['reset', '--hard', newRevision],
workingDirectory: workingDirectory,
);
if (result.exitCode != 0) {
throw ProcessException(
'git',
['reset', '--hard', newRevision],
'${result.stderr}',
result.exitCode,
);
}
}
}
@@ -0,0 +1,89 @@
import 'dart:io';
import 'package:shorebird_cli/src/command.dart';
mixin ShorebirdVersionMixin on ShorebirdCommand {
/// Whether the current version of Shorebird is the latest available.
Future<bool> isShorebirdVersionCurrent({
required String workingDirectory,
}) async {
final currentVersion = await fetchCurrentGitHash(
workingDirectory: workingDirectory,
);
final latestVersion = await fetchLatestGitHash(
workingDirectory: workingDirectory,
);
return currentVersion == latestVersion;
}
/// Returns the remote HEAD shorebird hash.
///
/// Exits if HEAD isn't pointing to a branch, or there is no upstream.
Future<String> fetchLatestGitHash({required String workingDirectory}) async {
// Fetch upstream branch's commits and tags
await runProcess(
'git',
['fetch', '--tags'],
workingDirectory: workingDirectory,
);
// Get the latest commit revision of the upstream
return _gitRevParse('@{upstream}', workingDirectory: workingDirectory);
}
/// Returns the local HEAD shorebird hash.
///
/// Exits if HEAD isn't pointing to a branch, or there is no upstream.
Future<String> fetchCurrentGitHash({
required String workingDirectory,
}) async {
// Get the commit revision of HEAD
return _gitRevParse('HEAD', workingDirectory: workingDirectory);
}
Future<String> _gitRevParse(
String revision, {
String? workingDirectory,
}) async {
// Get the commit revision of HEAD
final result = await runProcess(
'git',
['rev-parse', '--verify', revision],
workingDirectory: workingDirectory,
);
if (result.exitCode != 0) {
throw ProcessException(
'git',
['rev-parse', '--verify', revision],
'${result.stderr}',
result.exitCode,
);
}
return '${result.stdout}'.trim();
}
/// Attempts a hard reset to the given revision.
///
/// This is a reset instead of fast forward because if we are on a release
/// branch with cherry picks, there may not be a direct fast-forward route
/// to the next release.
Future<void> attemptReset({
required String newRevision,
required String workingDirectory,
}) async {
final result = await runProcess(
'git',
['reset', '--hard', newRevision],
workingDirectory: workingDirectory,
);
if (result.exitCode != 0) {
throw ProcessException(
'git',
['reset', '--hard', newRevision],
'${result.stderr}',
result.exitCode,
);
}
}
}
@@ -0,0 +1,86 @@
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shorebird_cli/src/commands/commands.dart';
import 'package:test/test.dart';
class _MockLogger extends Mock implements Logger {}
class _MockProcessResult extends Mock implements ProcessResult {}
void main() {
const currentShorebirdRevision = 'revision-1';
const newerShorebirdRevision = 'revision-2';
group('doctor', () {
late Logger logger;
late DoctorCommand command;
late ProcessResult fetchCurrentVersionResult;
late ProcessResult fetchLatestVersionResult;
setUp(() {
logger = _MockLogger();
fetchCurrentVersionResult = _MockProcessResult();
fetchLatestVersionResult = _MockProcessResult();
command = DoctorCommand(
logger: logger,
runProcess: (
executable,
arguments, {
bool runInShell = false,
workingDirectory,
}) async {
if (executable == 'git') {
const revParseHead = ['rev-parse', '--verify', 'HEAD'];
if (arguments.every((arg) => revParseHead.contains(arg))) {
return fetchCurrentVersionResult;
}
const revParseUpstream = ['rev-parse', '--verify', '@{upstream}'];
if (arguments.every((arg) => revParseUpstream.contains(arg))) {
return fetchLatestVersionResult;
}
}
return _MockProcessResult();
},
);
when(
() => fetchCurrentVersionResult.exitCode,
).thenReturn(ExitCode.success.code);
when(
() => fetchCurrentVersionResult.stdout,
).thenReturn(currentShorebirdRevision);
when(
() => fetchLatestVersionResult.exitCode,
).thenReturn(ExitCode.success.code);
when(
() => fetchLatestVersionResult.stdout,
).thenReturn(currentShorebirdRevision);
});
test('prints "no issues" when everything is OK', () async {
await command.run();
verify(
() => logger.info(captureAny(that: contains('No issues detected'))),
).called(1);
});
test('prints that an upgrade is available', () async {
when(
() => fetchLatestVersionResult.stdout,
).thenReturn(newerShorebirdRevision);
await command.run();
verify(
() => logger.info(
captureAny(
that: contains('A new version of shorebird is available!'),
),
),
).called(1);
});
});
}