feat: upgrade shorebird tools to new API (#52)

This commit is contained in:
Felix Angelov
2023-03-09 17:38:01 -06:00
committed by GitHub
parent 95b1e69da8
commit f9f9897b53
21 changed files with 500 additions and 87 deletions
+16
View File
@@ -0,0 +1,16 @@
targets:
$default:
builders:
source_gen|combining_builder:
options:
ignore_for_file:
- implicit_dynamic_parameter
- require_trailing_commas
- cast_nullable_to_non_nullable
- lines_longer_than_80_chars
- strict_raw_type
json_serializable:
options:
field_rename: snake
checked: true
explicit_to_json: true
+8 -3
View File
@@ -6,6 +6,7 @@ import 'package:mason_logger/mason_logger.dart';
import 'package:meta/meta.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_code_push_api_client/shorebird_code_push_api_client.dart';
import 'package:uuid/uuid.dart';
typedef CodePushClientBuilder = ShorebirdCodePushApiClient Function({
required String apiKey,
@@ -23,25 +24,29 @@ typedef RunProcess = Future<ProcessResult> Function(
bool runInShell,
});
typedef UuidBuilder = String Function();
abstract class ShorebirdCommand extends Command<int> {
ShorebirdCommand({
required this.logger,
Auth? auth,
CodePushClientBuilder? buildCodePushClient,
Logger? logger,
RunProcess? runProcess,
StartProcess? startProcess,
UuidBuilder? buildUuid,
}) : auth = auth ?? Auth(),
buildCodePushClient =
buildCodePushClient ?? ShorebirdCodePushApiClient.new,
logger = logger ?? Logger(),
runProcess = runProcess ?? Process.run,
startProcess = startProcess ?? Process.start;
startProcess = startProcess ?? Process.start,
buildUuid = buildUuid ?? const Uuid().v4;
final Auth auth;
final CodePushClientBuilder buildCodePushClient;
final Logger logger;
final RunProcess runProcess;
final StartProcess startProcess;
final UuidBuilder buildUuid;
/// [ArgResults] used for testing purposes only.
@visibleForTesting
@@ -35,12 +35,12 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
help: 'Noisy logging, including all shell commands executed.',
);
addCommand(BuildCommand());
addCommand(LoginCommand());
addCommand(LogoutCommand());
addCommand(PublishCommand());
addCommand(RunCommand());
addCommand(UpdateCommand(logger: logger, pubUpdater: pubUpdater));
addCommand(BuildCommand(logger: _logger));
addCommand(LoginCommand(logger: _logger));
addCommand(LogoutCommand(logger: _logger));
addCommand(PublishCommand(logger: _logger));
addCommand(RunCommand(logger: _logger));
addCommand(UpdateCommand(logger: _logger, pubUpdater: pubUpdater));
}
@override
@@ -18,9 +18,9 @@ typedef RunProcess = Future<ProcessResult> Function(
class BuildCommand extends ShorebirdCommand with ShorebirdEngineMixin {
/// {@macro build_command}
BuildCommand({
required super.logger,
super.auth,
super.buildCodePushClient,
super.logger,
super.runProcess,
});
@@ -8,7 +8,7 @@ import 'package:shorebird_cli/src/command.dart';
/// {@endtemplate}
class LoginCommand extends ShorebirdCommand {
/// {@macro login_command}
LoginCommand({super.auth, super.logger});
LoginCommand({required super.logger, super.auth});
@override
String get description => 'Login as a new Shorebird user.';
@@ -8,7 +8,7 @@ import 'package:shorebird_cli/src/command.dart';
/// {@endtemplate}
class LogoutCommand extends ShorebirdCommand {
/// {@macro logout_command}
LogoutCommand({super.auth, super.logger});
LogoutCommand({required super.logger, super.auth});
@override
String get description => 'Logout of the current Shorebird user';
@@ -1,8 +1,13 @@
import 'dart:io';
import 'package:checked_yaml/checked_yaml.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/config/shorebird_yaml.dart';
import 'package:yaml/yaml.dart';
import 'package:yaml_edit/yaml_edit.dart';
/// {@template publish_command}
///
@@ -11,7 +16,12 @@ import 'package:shorebird_cli/src/command.dart';
/// {@endtemplate}
class PublishCommand extends ShorebirdCommand {
/// {@macro publish_command}
PublishCommand({super.auth, super.buildCodePushClient, super.logger});
PublishCommand({
required super.logger,
super.auth,
super.buildCodePushClient,
super.buildUuid,
});
@override
String get description => 'Publish an update.';
@@ -32,7 +42,46 @@ class PublishCommand extends ShorebirdCommand {
usageException('A single file path must be specified.');
}
final releasePath = args.isEmpty
late final Pubspec? pubspecYaml;
try {
pubspecYaml = _readPubspecYaml();
if (pubspecYaml == null) {
logger.err('Could not find a "pubspec.yaml".');
return ExitCode.noInput.code;
}
} catch (error) {
logger.err('Error parsing "pubspec.yaml": $error');
return ExitCode.software.code;
}
late final String productId;
late final ShorebirdYaml? shorebirdYaml;
try {
shorebirdYaml = _readShorebirdYaml();
} catch (error) {
logger.err('Error parsing "shorebird.yaml": $error');
return ExitCode.software.code;
}
if (shorebirdYaml == null) {
productId = buildUuid();
File(
p.join(Directory.current.path, 'shorebird.yaml'),
).writeAsStringSync('''
# This file is used to configure the Shorebird CLI.
# Learn more at https://shorebird.dev
# This is the unique identifier for your app.
product_id: $productId
''');
logger.info('Generated a "shorebird.yaml".');
} else {
productId = shorebirdYaml.productId;
}
_addShorebirdYamlToAssets();
final artifactPath = args.isEmpty
? p.join(
Directory.current.path,
'build',
@@ -47,15 +96,23 @@ class PublishCommand extends ShorebirdCommand {
)
: args.first;
final artifact = File(releasePath);
final artifact = File(artifactPath);
if (!artifact.existsSync()) {
logger.err('File not found: ${artifact.path}');
logger.err('Artifact not found: "${artifact.path}"');
return ExitCode.noInput.code;
}
try {
final codePushClient = buildCodePushClient(apiKey: session.apiKey);
await codePushClient.createRelease(artifact.path);
logger.detail(
'Deploying ${artifact.path} to $productId (${pubspecYaml.version})',
);
await codePushClient.createPatch(
artifactPath: artifact.path,
baseVersion: pubspecYaml.version.toString(),
productId: productId,
channel: 'stable',
);
} catch (error) {
logger.err('Failed to deploy: $error');
return ExitCode.software.code;
@@ -64,4 +121,48 @@ class PublishCommand extends ShorebirdCommand {
logger.success('Deployed ${artifact.path}!');
return ExitCode.success.code;
}
ShorebirdYaml? _readShorebirdYaml() {
final file = File(p.join(Directory.current.path, 'shorebird.yaml'));
if (!file.existsSync()) return null;
final yaml = file.readAsStringSync();
return checkedYamlDecode(yaml, (m) => ShorebirdYaml.fromJson(m!));
}
Pubspec? _readPubspecYaml() {
final file = File(p.join(Directory.current.path, 'pubspec.yaml'));
if (!file.existsSync()) return null;
final yaml = file.readAsStringSync();
return Pubspec.parse(yaml);
}
void _addShorebirdYamlToAssets() {
final pubspecFile = File(p.join(Directory.current.path, 'pubspec.yaml'));
final pubspecContents = pubspecFile.readAsStringSync();
final yaml = loadYaml(pubspecContents, sourceUrl: pubspecFile.uri) as Map;
final editor = YamlEditor(pubspecContents);
if (!yaml.containsKey('flutter')) {
editor.update(
['flutter'],
{
'assets': ['shorebird.yaml']
},
);
} else {
if (!(yaml['flutter'] as Map).containsKey('assets')) {
editor.update(['flutter', 'assets'], ['shorebird.yaml']);
} else {
final assets = (yaml['flutter'] as Map)['assets'] as List;
if (!assets.contains('shorebird.yaml')) {
editor.update(['flutter', 'assets'], [...assets, 'shorebird.yaml']);
}
}
}
if (editor.edits.isNotEmpty) {
pubspecFile.writeAsStringSync(editor.toString());
logger.info('Added "shorebird.yaml" to "pubspec.yaml".');
}
}
}
@@ -19,9 +19,9 @@ typedef StartProcess = Future<Process> Function(
class RunCommand extends ShorebirdCommand with ShorebirdEngineMixin {
/// {@macro run_command}
RunCommand({
required super.logger,
super.auth,
super.buildCodePushClient,
super.logger,
super.startProcess,
});
@@ -12,7 +12,7 @@ import 'package:shorebird_cli/src/version.dart';
class UpdateCommand extends ShorebirdCommand {
/// {@macro update_command}
UpdateCommand({
super.logger,
required super.logger,
PubUpdater? pubUpdater,
}) : _pubUpdater = pubUpdater ?? PubUpdater();
@@ -0,0 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
part 'shorebird_yaml.g.dart';
@JsonSerializable(
anyMap: true,
disallowUnrecognizedKeys: true,
createToJson: false,
)
class ShorebirdYaml {
const ShorebirdYaml({required this.productId});
factory ShorebirdYaml.fromJson(Map<dynamic, dynamic> json) =>
_$ShorebirdYamlFromJson(json);
final String productId;
}
@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, strict_raw_type
part of 'shorebird_yaml.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ShorebirdYaml _$ShorebirdYamlFromJson(Map json) => $checkedCreate(
'ShorebirdYaml',
json,
($checkedConvert) {
$checkKeys(
json,
allowedKeys: const ['product_id'],
);
final val = ShorebirdYaml(
productId: $checkedConvert('product_id', (v) => v as String),
);
return val;
},
fieldKeyMap: const {'productId': 'product_id'},
);
+7
View File
@@ -11,19 +11,26 @@ environment:
dependencies:
archive: ^3.3.6
args: ^2.3.1
checked_yaml: ^2.0.2
cli_completion: ^0.3.0
cli_util: ^0.4.0
json_annotation: ^4.8.0
mason_logger: ^0.2.4
meta: ^1.9.0
path: ^1.8.3
pub_updater: ^0.2.4
pubspec_parse: ^1.2.2
shorebird_code_push_api_client:
path: ../shorebird_code_push_api_client
uuid: ^3.0.7
yaml: ^3.1.1
yaml_edit: ^2.1.0
dev_dependencies:
build_runner: ^2.0.0
build_verify: ^3.0.0
build_version: ^2.0.0
json_serializable: ^6.6.1
mocktail: ^0.3.0
test: ^1.19.2
very_good_analysis: ^4.0.0
View File
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
@@ -31,6 +33,13 @@ void main() {
projectId: 'test-project-id',
apiKey: 'test-api-key',
);
const productId = 'test-product-id';
const version = '1.2.3';
const pubspecYamlContent = '''
name: example
version: $version
environment:
sdk: ">=2.19.0 <3.0.0"''';
late ArgResults argResults;
late Auth auth;
@@ -51,7 +60,17 @@ void main() {
..testArgResults = argResults
..testCommandRunner = _FakeCommandRunner();
when(() => argResults.rest).thenReturn([]);
when(() => auth.currentSession).thenReturn(session);
when(() => logger.progress(any())).thenReturn(_MockProgress());
when(
() => codePushClient.createPatch(
baseVersion: any(named: 'baseVersion'),
artifactPath: any(named: 'artifactPath'),
channel: any(named: 'channel'),
productId: any(named: 'productId'),
),
).thenAnswer((_) async {});
});
test('throws no user error when session does not exist', () async {
@@ -61,48 +80,248 @@ void main() {
});
test('throws usage error when multiple args are passed.', () async {
when(() => auth.currentSession).thenReturn(session);
when(() => argResults.rest).thenReturn(['arg1', 'arg2']);
await expectLater(command.run, throwsA(isA<UsageException>()));
});
test('throws no input error when file is not found (default).', () async {
when(() => auth.currentSession).thenReturn(session);
when(() => argResults.rest).thenReturn([]);
final exitCode = await command.run();
test('throws no input error when pubspec.yaml is not found.', () async {
final tempDir = Directory.systemTemp.createTempSync();
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => logger.err('Could not find a "pubspec.yaml".')).called(1);
expect(exitCode, ExitCode.noInput.code);
});
test('throws software error when pubspec.yaml is malformed.', () async {
final tempDir = Directory.systemTemp.createTempSync();
File(p.join(tempDir.path, 'pubspec.yaml')).createSync();
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(
() => logger.err(any(that: contains('File not found: '))),
() => logger.err(any(that: contains('Error parsing "pubspec.yaml":'))),
).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws software error when shorebird.yaml is malformed.', () async {
final tempDir = Directory.systemTemp.createTempSync();
File(
p.join(tempDir.path, 'pubspec.yaml'),
).writeAsStringSync(pubspecYamlContent);
File(p.join(tempDir.path, 'shorebird.yaml')).createSync();
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(
() => logger.err(
any(that: contains('Error parsing "shorebird.yaml":')),
),
).called(1);
expect(exitCode, ExitCode.software.code);
});
test('throws no input error when artifact is not found (default).',
() async {
final tempDir = Directory.systemTemp.createTempSync();
File(
p.join(tempDir.path, 'pubspec.yaml'),
).writeAsStringSync(pubspecYamlContent);
File(
p.join(tempDir.path, 'shorebird.yaml'),
).writeAsStringSync('product_id: $productId');
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(
() => logger.err(any(that: contains('Artifact not found:'))),
).called(1);
expect(exitCode, ExitCode.noInput.code);
});
test('throws no input error when file is not found (custom).', () async {
when(() => auth.currentSession).thenReturn(session);
when(() => argResults.rest).thenReturn(['missing.txt']);
final exitCode = await command.run();
verify(() => logger.err('File not found: missing.txt')).called(1);
test('throws no input error when artifact is not found (custom).',
() async {
final tempDir = Directory.systemTemp.createTempSync();
final artifact = File(p.join(tempDir.path, 'patch.txt'));
when(() => argResults.rest).thenReturn([artifact.path]);
File(
p.join(tempDir.path, 'pubspec.yaml'),
).writeAsStringSync(pubspecYamlContent);
File(
p.join(tempDir.path, 'shorebird.yaml'),
).writeAsStringSync('product_id: $productId');
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(
() => logger.err(
any(
that: contains('Artifact not found: "${artifact.path}"'),
),
),
).called(1);
expect(exitCode, ExitCode.noInput.code);
});
test('throws error when release fails.', () async {
when(() => auth.currentSession).thenReturn(session);
test('throws error when publish fails.', () async {
const error = 'something went wrong';
when(() => codePushClient.createRelease(any())).thenThrow(error);
final release = p.join('test', 'fixtures', 'release.txt');
when(() => argResults.rest).thenReturn([release]);
final exitCode = await command.run();
when(
() => codePushClient.createPatch(
baseVersion: any(named: 'baseVersion'),
artifactPath: any(named: 'artifactPath'),
channel: any(named: 'channel'),
productId: any(named: 'productId'),
),
).thenThrow(error);
final tempDir = Directory.systemTemp.createTempSync();
final artifact = File(p.join(tempDir.path, 'patch.txt'))..createSync();
when(() => argResults.rest).thenReturn([artifact.path]);
File(
p.join(tempDir.path, 'pubspec.yaml'),
).writeAsStringSync(pubspecYamlContent);
File(
p.join(tempDir.path, 'shorebird.yaml'),
).writeAsStringSync('product_id: $productId');
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => logger.err('Failed to deploy: $error')).called(1);
expect(exitCode, ExitCode.software.code);
});
test('succeeds when release is successful.', () async {
when(() => auth.currentSession).thenReturn(session);
when(() => codePushClient.createRelease(any())).thenAnswer((_) async {});
final release = p.join('test', 'fixtures', 'release.txt');
when(() => argResults.rest).thenReturn([release]);
final exitCode = await command.run();
verify(() => logger.success('Deployed $release!')).called(1);
test('succeeds when publish is successful using existing product id',
() async {
final tempDir = Directory.systemTemp.createTempSync();
final artifact = File(p.join(tempDir.path, 'patch.txt'))..createSync();
when(() => argResults.rest).thenReturn([artifact.path]);
File(
p.join(tempDir.path, 'pubspec.yaml'),
).writeAsStringSync(pubspecYamlContent);
File(
p.join(tempDir.path, 'shorebird.yaml'),
).writeAsStringSync('product_id: $productId');
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => logger.success('Deployed ${artifact.path}!')).called(1);
verify(
() => codePushClient.createPatch(
baseVersion: version,
productId: productId,
artifactPath: artifact.path,
channel: 'stable',
),
).called(1);
expect(exitCode, ExitCode.success.code);
});
test('succeeds when publish is successful using newly generated product id',
() async {
final tempDir = Directory.systemTemp.createTempSync();
final artifact = File(p.join(tempDir.path, 'patch.txt'))..createSync();
when(() => argResults.rest).thenReturn([artifact.path]);
File(
p.join(tempDir.path, 'pubspec.yaml'),
).writeAsStringSync(pubspecYamlContent);
final exitCode = await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
verify(() => logger.success('Deployed ${artifact.path}!')).called(1);
verify(
() => codePushClient.createPatch(
baseVersion: version,
productId: any(named: 'productId', that: isNotEmpty),
artifactPath: artifact.path,
channel: 'stable',
),
).called(1);
expect(
File(p.join(tempDir.path, 'shorebird.yaml')).existsSync(),
isTrue,
);
expect(exitCode, ExitCode.success.code);
});
test('creates flutter.assets and adds shorebird.yaml', () async {
final tempDir = Directory.systemTemp.createTempSync();
final artifact = File(p.join(tempDir.path, 'patch.txt'))..createSync();
when(() => argResults.rest).thenReturn([artifact.path]);
File(
p.join(tempDir.path, 'pubspec.yaml'),
).writeAsStringSync(pubspecYamlContent);
await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
expect(
File(p.join(tempDir.path, 'pubspec.yaml')).readAsStringSync(),
equals('''
$pubspecYamlContent
flutter:
assets:
- shorebird.yaml
'''),
);
});
test('creates assets and adds shorebird.yaml', () async {
final tempDir = Directory.systemTemp.createTempSync();
final artifact = File(p.join(tempDir.path, 'patch.txt'))..createSync();
when(() => argResults.rest).thenReturn([artifact.path]);
File(p.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync('''
$pubspecYamlContent
flutter:
uses-material-design: true
''');
await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
expect(
File(p.join(tempDir.path, 'pubspec.yaml')).readAsStringSync(),
equals('''
$pubspecYamlContent
flutter:
assets:
- shorebird.yaml
uses-material-design: true
'''),
);
});
test('adds shorebird.yaml to assets', () async {
final tempDir = Directory.systemTemp.createTempSync();
final artifact = File(p.join(tempDir.path, 'patch.txt'))..createSync();
when(() => argResults.rest).thenReturn([artifact.path]);
File(p.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync('''
$pubspecYamlContent
flutter:
assets:
- some/asset.txt
''');
await IOOverrides.runZoned(
command.run,
getCurrentDirectory: () => tempDir,
);
expect(
File(p.join(tempDir.path, 'pubspec.yaml')).readAsStringSync(),
equals('''
$pubspecYamlContent
flutter:
assets:
- some/asset.txt
- shorebird.yaml
'''),
);
});
});
}
@@ -1,6 +1,6 @@
// ignore_for_file: unused_local_variable
import 'package:shorebird_code_push_api_client/src/shorebird_code_push_api_client.dart';
import 'package:shorebird_code_push_api_client/shorebird_code_push_api_client.dart';
Future<void> main() async {
final client = ShorebirdCodePushApiClient(apiKey: '<API KEY>');
@@ -8,6 +8,11 @@ Future<void> main() async {
// Download the latest engine revision.
final engine = await client.downloadEngine('latest');
// Create a new release.
await client.createRelease('path/to/release/libapp.so');
// Create a new patch.
await client.createPatch(
artifactPath: '<PATH TO ARTIFACT>', // e.g. 'libapp.so'
baseVersion: '<BASE VERSION>', // e.g. '1.0.0'
productId: '<PRODUCT ID>', // e.g. 'shorebird-example'
channel: '<CHANNEL>', // e.g. 'stable'
);
}
@@ -14,24 +14,35 @@ class ShorebirdCodePushApiClient {
Uri? hostedUri,
}) : _apiKey = apiKey,
_httpClient = httpClient ?? http.Client(),
_hostedUri = hostedUri ??
Uri.https('shorebird-code-push-api-cypqazu4da-uc.a.run.app');
hostedUri =
hostedUri ?? Uri.https('code-push-server-kmdbqkx7rq-uc.a.run.app');
final String _apiKey;
final http.Client _httpClient;
final Uri _hostedUri;
/// The hosted uri for the Shorebird CodePush API.
final Uri hostedUri;
Map<String, String> get _apiKeyHeader => {'x-api-key': _apiKey};
/// Upload the artifact at [path] to the
/// Shorebird CodePush API as a new release.
Future<void> createRelease(String path) async {
/// Create a new patch.
Future<void> createPatch({
required String baseVersion,
required String productId,
required String channel,
required String artifactPath,
}) async {
final request = http.MultipartRequest(
'POST',
Uri.parse('$_hostedUri/api/v1/releases'),
Uri.parse('$hostedUri/api/v1/patches'),
);
final file = await http.MultipartFile.fromPath('file', path);
final file = await http.MultipartFile.fromPath('file', artifactPath);
request.files.add(file);
request.fields.addAll({
'base_version': baseVersion,
'product_id': productId,
'channel': channel,
});
request.headers.addAll(_apiKeyHeader);
final response = await _httpClient.send(request);
@@ -44,7 +55,10 @@ class ShorebirdCodePushApiClient {
Future<Uint8List> downloadEngine(String revision) async {
final request = http.Request(
'GET',
Uri.parse('$_hostedUri/api/v1/engines/$revision'),
Uri.parse(
// TODO(felangel): use the revision instead of hardcoded "dev".
'https://storage.googleapis.com/code-push-dev.appspot.com/engines/dev/engine.zip',
),
);
request.headers.addAll(_apiKeyHeader);
@@ -34,7 +34,7 @@ void main() {
expect(ShorebirdCodePushApiClient(apiKey: apiKey), isNotNull);
});
group('createRelease', () {
group('createPatch', () {
test('throws an exception if the http request fails', () async {
when(() => httpClient.send(any())).thenAnswer((_) async {
return http.StreamedResponse(
@@ -44,8 +44,11 @@ void main() {
});
expect(
shorebirdCodePushApiClient.createRelease(
path.join('test', 'fixtures', 'release.txt'),
shorebirdCodePushApiClient.createPatch(
artifactPath: path.join('test', 'fixtures', 'release.txt'),
baseVersion: '1.0.0',
productId: 'shorebird-example',
channel: 'stable',
),
throwsA(isA<Exception>()),
);
@@ -59,8 +62,11 @@ void main() {
);
});
await shorebirdCodePushApiClient.createRelease(
path.join('test', 'fixtures', 'release.txt'),
await shorebirdCodePushApiClient.createPatch(
artifactPath: path.join('test', 'fixtures', 'release.txt'),
baseVersion: '1.0.0',
productId: 'shorebird-example',
channel: 'stable',
);
final request = verify(() => httpClient.send(captureAny()))
@@ -68,9 +74,7 @@ void main() {
.single as http.MultipartRequest;
expect(
request.url,
Uri.parse(
'https://shorebird-code-push-api-cypqazu4da-uc.a.run.app/api/v1/releases',
),
shorebirdCodePushApiClient.hostedUri.replace(path: '/api/v1/patches'),
);
});
});
@@ -108,7 +112,7 @@ void main() {
expect(
request.url,
Uri.parse(
'https://shorebird-code-push-api-cypqazu4da-uc.a.run.app/api/v1/engines/$engineRevision',
'https://storage.googleapis.com/code-push-dev.appspot.com/engines/dev/engine.zip',
),
);
});
+8 -8
View File
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::network::UpdateResponse;
use crate::network::PatchCheckResponse;
pub struct PatchInfo {
pub path: String,
@@ -114,18 +114,18 @@ pub fn download_file_to_path(url: &str, path: &PathBuf) -> anyhow::Result<()> {
pub fn download_into_unused_slot(
cache_dir: &str,
update_response: &UpdateResponse,
patch_check_response: &PatchCheckResponse,
state: &mut UpdaterState,
) -> anyhow::Result<usize> {
// Download the new version into the unused slot.
let slot_index = unused_slot(state);
download_into_slot(cache_dir, update_response, state, slot_index)?;
download_into_slot(cache_dir, patch_check_response, state, slot_index)?;
Ok(slot_index)
}
fn download_into_slot(
cache_dir: &str,
update_response: &UpdateResponse,
patch_check_response: &PatchCheckResponse,
state: &mut UpdaterState,
slot_index: usize,
) -> anyhow::Result<()> {
@@ -136,14 +136,14 @@ fn download_into_slot(
.join("libapp.txt");
// TODO: Shouldn't crash on malformed response.
let update = update_response.update.as_ref().unwrap();
let patch = patch_check_response.patch.as_ref().unwrap();
// We should download into a separate place and move into place.
// That would allow us to check the hash before moving into place.
// Would also allow the move/state update to be "atomic" or at least allow
// us to carefully guard against state corruption.
// Would also let us support when we need to allow the system to download for us (e.g. iOS).
download_file_to_path(&update.download_url, &path)?;
download_file_to_path(&patch.download_url, &path)?;
// Check the hash against the download?
// Update the state to include the new version.
@@ -152,8 +152,8 @@ fn download_into_slot(
slot_index,
Slot {
path: path.to_str().unwrap().to_string(),
version: update.version.clone(),
hash: update.hash.clone(),
version: patch.version.clone(),
hash: patch.hash.clone(),
},
);
save_state(&state, cache_dir)?;
+1 -1
View File
@@ -8,7 +8,7 @@ use once_cell::sync::OnceCell;
// cbindgen looks for const, ignore these so it doesn't warn about them.
/// cbindgen:ignore
const DEFAULT_BASE_URL: &'static str = "https://shorebird-code-push-api-cypqazu4da-uc.a.run.app";
const DEFAULT_BASE_URL: &'static str = "https://code-push-server-kmdbqkx7rq-uc.a.run.app";
/// cbindgen:ignore
const DEFAULT_CHANNEL: &'static str = "stable";
+9 -9
View File
@@ -9,28 +9,28 @@ use serde::Deserialize;
use crate::cache::PatchInfo;
use crate::config::ResolvedConfig;
fn updates_url(base_url: &str) -> String {
return format!("{}/api/v1/updates", base_url);
fn patches_check_url(base_url: &str) -> String {
return format!("{}/api/v1/patches/check", base_url);
}
#[derive(Deserialize)]
pub struct Update {
pub struct Patch {
pub version: String,
pub hash: String,
pub download_url: String,
}
#[derive(Deserialize)]
pub struct UpdateResponse {
pub update_available: bool,
pub struct PatchCheckResponse {
pub patch_available: bool,
#[serde(default)]
pub update: Option<Update>,
pub patch: Option<Patch>,
}
pub fn send_update_request(
pub fn send_patch_check_request(
config: &ResolvedConfig,
patch: Option<PatchInfo>,
) -> anyhow::Result<UpdateResponse> {
) -> anyhow::Result<PatchCheckResponse> {
#[cfg(target_os = "macos")]
static PLATFORM: &str = "macos";
#[cfg(target_os = "linux")]
@@ -61,7 +61,7 @@ pub fn send_update_request(
body.insert("platform", PLATFORM.to_string());
body.insert("arch", ARCH.to_string());
let response = client
.post(&updates_url(&config.base_url))
.post(&patches_check_url(&config.base_url))
.json(&body)
.send()?
.json()?;
+5 -5
View File
@@ -8,7 +8,7 @@ use crate::cache::{
};
use crate::config::{set_config, with_config, ResolvedConfig};
use crate::logging::init_logging;
use crate::network::send_update_request;
use crate::network::send_patch_check_request;
pub enum UpdateStatus {
NoUpdate,
@@ -56,14 +56,14 @@ pub fn check_for_update_internal(config: &ResolvedConfig) -> bool {
// Check the current slot.
let patch = current_patch_internal(&state);
// Send info from app + current slot to server.
let response_result = send_update_request(&config, patch);
let response_result = send_patch_check_request(&config, patch);
match response_result {
Err(err) => {
error!("Failed update check: {err}");
return false;
}
Ok(response) => {
return response.update_available;
return response.patch_available;
}
}
}
@@ -77,8 +77,8 @@ fn update_internal(config: &ResolvedConfig) -> anyhow::Result<UpdateStatus> {
let mut state = load_state(&config.cache_dir).unwrap_or_default();
let version = current_patch_internal(&state);
// Check for update.
let response = send_update_request(&config, version)?;
if !response.update_available {
let response = send_patch_check_request(&config, version)?;
if !response.patch_available {
return Ok(UpdateStatus::NoUpdate);
}
// If needed, download the new version.