feat: Add simple shorebird doctor command that checks for updates (#213)
Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user