diff --git a/.github/workflows/shorebird_ci.yml b/.github/workflows/shorebird_ci.yml index 03453aae11924..601329c8f5305 100644 --- a/.github/workflows/shorebird_ci.yml +++ b/.github/workflows/shorebird_ci.yml @@ -1,4 +1,4 @@ -name: shorebird_ci +name: shorebird_tests concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -6,9 +6,25 @@ concurrency: on: pull_request: + paths: + - ".github/workflows/shorebird_tests.yaml" + - "packages/shorebird_tests/lib/**" + - "packages/shorebird_tests/test/**" + - "packages/shorebird_tests/pubspec.yaml" + - "packages/flutter_tools/lib/**" + - "packages/flutter_tools/test/**" + - "packages/flutter_tools/pubspec.yaml" push: branches: - main + paths: + - ".github/workflows/shorebird_tests.yaml" + - "packages/shorebird_tests/lib/**" + - "packages/shorebird_tests/test/**" + - "packages/shorebird_tests/pubspec.yaml" + - "packages/flutter_tools/lib/**" + - "packages/flutter_tools/test/**" + - "packages/flutter_tools/pubspec.yaml" jobs: test: diff --git a/.github/workflows/shorebird_tools.yaml b/.github/workflows/shorebird_tools.yaml new file mode 100644 index 0000000000000..b9264ad963b73 --- /dev/null +++ b/.github/workflows/shorebird_tools.yaml @@ -0,0 +1,30 @@ +name: Shorebird Tools 💻 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + paths: + - ".github/workflows/shorebird_tools.yaml" + - "packages/shorebird_tools/lib/**" + - "packages/shorebird_tools/test/**" + - "packages/shorebird_tools/pubspec.yaml" + push: + branches: + - main + paths: + - ".github/workflows/shorebird_tools.yaml" + - "packages/shorebird_tools/lib/**" + - "packages/shorebird_tools/test/**" + - "packages/shorebird_tools/pubspec.yaml" + +jobs: + semantic-pull-request: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 + + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 + with: + working_directory: packages/shorebird_tools diff --git a/packages/shorebird_tools/lib/src/command_runner.dart b/packages/shorebird_tools/lib/src/command_runner.dart index 9b3a09a2ae5c5..e4a4cb1eb1814 100644 --- a/packages/shorebird_tools/lib/src/command_runner.dart +++ b/packages/shorebird_tools/lib/src/command_runner.dart @@ -36,6 +36,7 @@ class ShorebirdToolsCommandRunner extends CommandRunner { // Add sub commands addCommand(SampleCommand(logger: _logger)); + addCommand(PackageCommand(logger: _logger)); } @override diff --git a/packages/shorebird_tools/lib/src/commands/commands.dart b/packages/shorebird_tools/lib/src/commands/commands.dart index fc0403a8354fe..34ceda22503ab 100644 --- a/packages/shorebird_tools/lib/src/commands/commands.dart +++ b/packages/shorebird_tools/lib/src/commands/commands.dart @@ -1 +1,2 @@ +export 'package_command.dart'; export 'sample_command.dart'; diff --git a/packages/shorebird_tools/lib/src/commands/package_command.dart b/packages/shorebird_tools/lib/src/commands/package_command.dart new file mode 100644 index 0000000000000..c0e8f59e317c0 --- /dev/null +++ b/packages/shorebird_tools/lib/src/commands/package_command.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:args/command_runner.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:path/path.dart' as p; + +/// {@template package_command} +/// +/// `shorebird_tools package` +/// A [Command] that packages a generated patch into an archive +/// {@endtemplate} +class PackageCommand extends Command { + /// {@macro package_command} + PackageCommand({ + required Logger logger, + }) : _logger = logger { + argParser + ..addOption( + 'patch', + abbr: 'p', + mandatory: true, + help: 'The path to the patch artifact which will be packaged', + ) + ..addOption( + 'output', + abbr: 'o', + mandatory: true, + help: 'Where to write the packaged patch archive', + ); + } + + @override + String get description => 'Packages a patch artifact into an archive'; + + @override + String get name => 'package'; + + final Logger _logger; + + @override + Future run() async { + final patchFilePath = argResults!['patch'] as String; + final outFilePath = argResults!['output'] as String; + + final patchFile = File(patchFilePath); + if (!patchFile.existsSync()) { + _logger.err('Patch file not found at $patchFilePath'); + return ExitCode.software.code; + } + + final outFile = File(outFilePath); + + final bytes = patchFile.readAsBytesSync(); + final archiveFile = ArchiveFile( + p.basename(patchFilePath), + bytes.length, + bytes, + ); + + ZipEncoder() + ..startEncode(OutputFileStream(outFile.absolute.path)) + ..addFile(archiveFile) + ..endEncode(); + + _logger.info('Packaged patch at $patchFilePath to $outFilePath'); + + return ExitCode.success.code; + } +} diff --git a/packages/shorebird_tools/pubspec.yaml b/packages/shorebird_tools/pubspec.yaml index af140b04e5999..71cc792972c83 100644 --- a/packages/shorebird_tools/pubspec.yaml +++ b/packages/shorebird_tools/pubspec.yaml @@ -7,8 +7,10 @@ environment: sdk: "^3.3.0" dependencies: + archive: ^3.5.1 args: ^2.4.2 mason_logger: ^0.2.12 + path: ^1.9.0 dev_dependencies: build_runner: ^2.4.8 diff --git a/packages/shorebird_tools/test/src/command_runner_test.dart b/packages/shorebird_tools/test/src/command_runner_test.dart index 22ebba4f46078..6de70d8d68556 100644 --- a/packages/shorebird_tools/test/src/command_runner_test.dart +++ b/packages/shorebird_tools/test/src/command_runner_test.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:args/command_runner.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; @@ -9,12 +7,8 @@ import 'package:test/test.dart'; class _MockLogger extends Mock implements Logger {} -class _MockProgress extends Mock implements Progress {} - - const latestVersion = '0.0.0'; - void main() { group('ShorebirdToolsCommandRunner', () { late Logger logger; diff --git a/packages/shorebird_tools/test/src/commands/package_command_test.dart b/packages/shorebird_tools/test/src/commands/package_command_test.dart new file mode 100644 index 0000000000000..8775b8b09b114 --- /dev/null +++ b/packages/shorebird_tools/test/src/commands/package_command_test.dart @@ -0,0 +1,126 @@ +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as p; +import 'package:shorebird_tools/src/command_runner.dart'; +import 'package:test/test.dart'; + +class _MockLogger extends Mock implements Logger {} + +void main() { + group('package', () { + late Logger logger; + late ShorebirdToolsCommandRunner commandRunner; + late Directory testDir; + + setUp(() { + logger = _MockLogger(); + commandRunner = ShorebirdToolsCommandRunner(logger: logger); + testDir = Directory.systemTemp.createTempSync('shorebird_tools_test'); + }); + + test('packages the received patch', () async { + File(p.join(testDir.path, 'patch.txt')).writeAsStringSync('banana'); + + final exitCode = await commandRunner.run( + [ + 'package', + '-p', + p.join(testDir.path, 'patch.txt'), + '-o', + p.join(testDir.path, 'patch.zip'), + ], + ); + + expect(exitCode, ExitCode.success.code); + + verify( + () => logger.info( + 'Packaged patch at ${p.join(testDir.path, 'patch.txt')} ' + 'to ${p.join(testDir.path, 'patch.zip')}', + ), + ).called(1); + + final patchFile = File(p.join(testDir.path, 'patch.zip')); + expect(patchFile.existsSync(), isTrue); + + final extractedFolder = Directory(p.join(testDir.path, 'extracted')) + ..createSync(); + await extractFileToDisk(patchFile.path, extractedFolder.path); + + // Making sure it was correctly archived and can be extracted + final extractedFile = File(p.join(extractedFolder.path, 'patch.txt')); + expect(extractedFile.existsSync(), isTrue); + expect(extractedFile.readAsStringSync(), 'banana'); + }); + + group('when the patch file does not exists', () { + test('error and logs', () async { + final exitCode = await commandRunner.run( + [ + 'package', + '-p', + p.join(testDir.path, 'patch.txt'), + '-o', + p.join(testDir.path, 'patch.zip'), + ], + ); + + expect(exitCode, ExitCode.software.code); + + verify( + () => logger.err( + 'Patch file not found at ${p.join(testDir.path, 'patch.txt')}', + ), + ).called(1); + }); + }); + + group('when missing option for -p', () { + test('shows errors and print the correct usage', () async { + final exitCode = await commandRunner.run(['package', '-p']); + + expect(exitCode, ExitCode.usage.code); + + verify(() => logger.err('Missing argument for "patch".')).called(1); + verify( + () => logger.info(''' +Usage: shorebird_tools package [arguments] +-h, --help Print this usage information. +-p, --patch (mandatory) The path to the patch artifact which will be packaged +-o, --output (mandatory) Where to write the packaged patch archive + +Run "shorebird_tools help" to see global options.'''), + ).called(1); + }); + }); + + group('when missing option for -o', () { + test('shows errors and print the correct usage', () async { + final exitCode = await commandRunner.run( + [ + 'package', + '-p', + 'bla', + '-o', + ], + ); + + expect(exitCode, ExitCode.usage.code); + + verify(() => logger.err('Missing argument for "output".')).called(1); + verify( + () => logger.info(''' +Usage: shorebird_tools package [arguments] +-h, --help Print this usage information. +-p, --patch (mandatory) The path to the patch artifact which will be packaged +-o, --output (mandatory) Where to write the packaged patch archive + +Run "shorebird_tools help" to see global options.'''), + ).called(1); + }); + }); + }); +}