diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml new file mode 100644 index 0000000..c07258e --- /dev/null +++ b/.github/workflows/claude-review.yml @@ -0,0 +1,40 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + +jobs: + claude-review: + # Repo secrets are not available to PRs from forks; skip them. + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + Review this pull request as a senior Dart/Flutter engineer. Focus on: + - Correctness bugs and edge cases in the changed code + - API misuse and error-handling gaps + - Security issues (credential handling, injection, unsafe file I/O) + - Backwards compatibility for existing users of this package + + Use `gh pr comment` for overall feedback and + `mcp__github_inline_comment__create_inline_comment` (with confirmed: true) + for line-specific issues. Only post GitHub comments — do not submit + review text as plain messages. Be concise; skip pure style nits. + claude_args: | + --model claude-sonnet-4-6 + --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" diff --git a/lib/src/commands/codepush_commands/_codepush_login.dart b/lib/src/commands/codepush_commands/_codepush_login.dart index 2534ea7..7dfa9e6 100644 --- a/lib/src/commands/codepush_commands/_codepush_login.dart +++ b/lib/src/commands/codepush_commands/_codepush_login.dart @@ -69,9 +69,7 @@ class CodePushLoginSubCommand extends Command { final data = json.decode(body) as Map; if (response.statusCode != 201) { - _logger.err( - 'Failed to start login: ${data['error'] ?? 'Unknown'}', - ); + _logger.err('Failed to start login: ${data['error'] ?? 'Unknown'}'); return ExitCode.software.code; } @@ -83,7 +81,9 @@ class CodePushLoginSubCommand extends Command { // Step 2: Open browser. _logger.info('Your authorization code: $userCode'); _logger.info(''); - _logger.info('Authorization URL (click or copy to use a different browser):'); + _logger.info( + 'Authorization URL (click or copy to use a different browser):', + ); _logger.info(' $authorizeUrl'); _logger.info(''); @@ -91,7 +91,9 @@ class CodePushLoginSubCommand extends Command { await _openBrowser(authorizeUrl); _logger.info('Browser opened. Authorize the CLI there.'); } catch (_) { - _logger.info('Could not open browser automatically — use the URL above.'); + _logger.info( + 'Could not open browser automatically — use the URL above.', + ); } _logger.info(''); @@ -126,8 +128,9 @@ class CodePushLoginSubCommand extends Command { } if (status == 'expired') { - progress - .fail('Authorization expired. Run "fcp codepush login" again.'); + progress.fail( + 'Authorization expired. Run "fcp codepush login" again.', + ); return ExitCode.software.code; } diff --git a/lib/src/commands/codepush_commands/_codepush_patch.dart b/lib/src/commands/codepush_commands/_codepush_patch.dart index ad3558c..9e94bf4 100644 --- a/lib/src/commands/codepush_commands/_codepush_patch.dart +++ b/lib/src/commands/codepush_commands/_codepush_patch.dart @@ -13,10 +13,7 @@ import 'package:mason_logger/mason_logger.dart'; class CodePushPatchSubCommand extends Command { CodePushPatchSubCommand(this._logger) { argParser - ..addOption( - 'release-id', - help: 'The release ID to patch against.', - ) + ..addOption('release-id', help: 'The release ID to patch against.') ..addOption( 'patch-file', help: 'Path to an existing patch file to upload.', @@ -202,8 +199,10 @@ class CodePushPatchSubCommand extends Command { iosPatchSourceImport = importPathForPatchSource(patchSource); iosHelperImports = _discoverDirectHelpers(patchSource); if (iosHelperImports.isNotEmpty) { - _logger.detail('Discovered ${iosHelperImports.length} ' - 'helper import(s) from patch source'); + _logger.detail( + 'Discovered ${iosHelperImports.length} ' + 'helper import(s) from patch source', + ); } _logger.detail('Using iOS patch source: $patchSource'); _logger.detail('Generated iOS patch target: $generated'); @@ -311,8 +310,7 @@ class CodePushPatchSubCommand extends Command { final includeUris = { if (iosSwapMode && iosPatchSourceImport != null) '$packagePrefix$iosPatchSourceImport', - for (final helper in iosHelperImports) - '$packagePrefix$helper', + for (final helper in iosHelperImports) '$packagePrefix$helper', ...explicitIncludes.where((u) => u.isNotEmpty), }.toList(); @@ -626,8 +624,9 @@ class CodePushPatchSubCommand extends Command { if (!file.existsSync()) return null; try { for (final line in file.readAsLinesSync()) { - final match = - RegExp(r'^name:\s*([A-Za-z_][A-Za-z0-9_]*)\s*$').firstMatch(line); + final match = RegExp( + r'^name:\s*([A-Za-z_][A-Za-z0-9_]*)\s*$', + ).firstMatch(line); if (match != null) { return 'package:${match.group(1)}/'; } @@ -638,9 +637,7 @@ class CodePushPatchSubCommand extends Command { return null; } - String? _resolveIosPatchSource({ - String? explicitPath, - }) { + String? _resolveIosPatchSource({String? explicitPath}) { if (explicitPath != null && explicitPath.isNotEmpty) { if (!File(explicitPath).existsSync()) { _logger.err('Patch entry source not found: $explicitPath'); @@ -717,8 +714,9 @@ class CodePushPatchSubCommand extends Command { // Match: import 'relative/path.dart'; or import "../path.dart"; // Exclude: dart: and package: imports. - final importPattern = - RegExp(r'''import\s+['"](?!dart:|package:)([^'"]+)['"]'''); + final importPattern = RegExp( + r'''import\s+['"](?!dart:|package:)([^'"]+)['"]''', + ); for (final match in importPattern.allMatches(source)) { final relativePath = match.group(1); diff --git a/lib/src/commands/codepush_commands/_codepush_release.dart b/lib/src/commands/codepush_commands/_codepush_release.dart index 572efc7..3257c48 100644 --- a/lib/src/commands/codepush_commands/_codepush_release.dart +++ b/lib/src/commands/codepush_commands/_codepush_release.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:args/command_runner.dart'; +import 'package:flutter_compile/src/shared/android_baseline_yaml.dart'; import 'package:flutter_compile/src/shared/codepush_archive_service.dart'; import 'package:flutter_compile/src/shared/codepush_artifact_manager.dart'; import 'package:flutter_compile/src/shared/codepush_build_service.dart'; @@ -99,6 +100,7 @@ class CodePushReleaseSubCommand extends Command { final buildService = CodePushBuildService(logger: _logger); String? baselineId; String? originalIosInfoPlist; + String? originalAndroidYaml; String? builtPlatform; if (shouldBuild) { @@ -171,6 +173,26 @@ class CodePushReleaseSubCommand extends Command { } } + // Stamp the release version into the Android code push config + // asset BEFORE `flutter build`, so the shipped APK carries the + // version it is released as. Restored afterwards so the app repo + // stays clean (same pattern as the iOS Info.plist stamp above). + if ((platform == 'apk' || platform == 'appbundle') && + version.isNotEmpty) { + originalAndroidYaml = writeReleaseVersionToAndroidYaml(version); + if (originalAndroidYaml == null) { + _logger.warn( + 'Warning: $kDefaultAndroidCodePushYamlPath not found. ' + 'This build will not embed a release version. ' + 'Run "fcp codepush init" to set up Android.', + ); + } else { + _logger.detail( + 'Stamped release_version=$version into codepush.yaml', + ); + } + } + final buildProgress = _logger.progress('Building release ($platform)'); final buildOk = await buildService.buildRelease( platform: platform, @@ -205,6 +227,21 @@ class CodePushReleaseSubCommand extends Command { restoreIosInfoPlist(originalIosInfoPlist); _logger.detail('Restored ios/Runner/Info.plist'); } + if (originalAndroidYaml != null) { + try { + restoreAndroidYaml(originalAndroidYaml); + _logger.detail('Restored android assets/codepush.yaml'); + } on FileSystemException catch (e) { + // Don't mask an in-flight build error with a restore failure; + // tell the user the repo is dirty and how to fix it. + _logger.err( + 'Failed to restore $kDefaultAndroidCodePushYamlPath after the ' + 'build: $e\nThe file still contains the stamped release ' + 'version — restore it manually (e.g. git checkout -- ' + '$kDefaultAndroidCodePushYamlPath).', + ); + } + } } } diff --git a/lib/src/shared/android_baseline_yaml.dart b/lib/src/shared/android_baseline_yaml.dart new file mode 100644 index 0000000..6099cea --- /dev/null +++ b/lib/src/shared/android_baseline_yaml.dart @@ -0,0 +1,62 @@ +import 'dart:io'; + +const String kDefaultAndroidCodePushYamlPath = + 'android/app/src/main/assets/codepush.yaml'; + +/// Stamps [releaseVersion] into the Android code push config asset so the +/// built APK carries the version it is released as. Returns the original +/// file content for restoring afterwards, or `null` when the file does not +/// exist (code push has not been initialized for Android). +String? writeReleaseVersionToAndroidYaml( + String releaseVersion, { + String yamlPath = kDefaultAndroidCodePushYamlPath, +}) { + if (!RegExp(r'^[A-Za-z0-9._+\-]+$').hasMatch(releaseVersion)) { + throw ArgumentError.value( + releaseVersion, + 'releaseVersion', + 'contains characters that would break the YAML config', + ); + } + + final yamlFile = File(yamlPath); + if (!yamlFile.existsSync()) return null; + + final originalContent = yamlFile.readAsStringSync(); + var content = originalContent; + final versionLine = 'release_version: "$releaseVersion"'; + + if (RegExp(r'^release_version:', multiLine: true).hasMatch(content)) { + content = content.replaceFirst( + RegExp(r'^release_version:.*$', multiLine: true), + versionLine, + ); + } else { + if (content.isNotEmpty && !content.endsWith('\n')) content += '\n'; + content += '$versionLine\n'; + } + + // Write to a temp file and rename over the original: a failed write + // (disk full, permissions) can then never corrupt the config, and the + // original error propagates without a doomed in-place rescue attempt. + final tempFile = File('$yamlPath.tmp'); + try { + tempFile.writeAsStringSync(content); + tempFile.renameSync(yamlPath); + } on FileSystemException { + try { + tempFile.deleteSync(); + } on FileSystemException { + // Best-effort cleanup; the original config is untouched either way. + } + rethrow; + } + return originalContent; +} + +void restoreAndroidYaml( + String originalContent, { + String yamlPath = kDefaultAndroidCodePushYamlPath, +}) { + File(yamlPath).writeAsStringSync(originalContent); +} diff --git a/lib/src/shared/codepush_artifact_manager.dart b/lib/src/shared/codepush_artifact_manager.dart index cca91dd..160d855 100644 --- a/lib/src/shared/codepush_artifact_manager.dart +++ b/lib/src/shared/codepush_artifact_manager.dart @@ -53,8 +53,9 @@ class CodePushArtifactManager { final request = await client.getUrl(Uri.parse(url)); final response = await request.close(); if (response.statusCode != 200) { - progress - .fail('Build tool download failed (HTTP ${response.statusCode})'); + progress.fail( + 'Build tool download failed (HTTP ${response.statusCode})', + ); return null; } final sink = cached.openWrite(); @@ -285,9 +286,7 @@ class CodePushArtifactManager { ); return false; } - _logger.detail( - 'Using local iOS engine override from $overrideDirPath', - ); + _logger.detail('Using local iOS engine override from $overrideDirPath'); } else { // Always re-download iOS target files from GCS. A previous // `--force` run may have cached an older version and the @@ -323,8 +322,9 @@ class CodePushArtifactManager { } final engineCache = '$flutterRoot/bin/cache/artifacts/engine'; - final productSdkDir = - Directory('$engineCache/common/flutter_patched_sdk_product'); + final productSdkDir = Directory( + '$engineCache/common/flutter_patched_sdk_product', + ); final iosReleaseDir = Directory('$engineCache/ios-release'); final frameworkDir = Directory( '$engineCache/ios-release/Flutter.xcframework/ios-arm64/Flutter.framework', @@ -346,7 +346,8 @@ class CodePushArtifactManager { _logger.err('Missing one or more iOS overlay files.'); return false; } - if (!usingLocalOverride && (xcframeworkTar == null || !xcframeworkTar.existsSync())) { + if (!usingLocalOverride && + (xcframeworkTar == null || !xcframeworkTar.existsSync())) { _logger.err('Missing overlay file: ${xcframeworkTar?.path ?? 'unknown'}'); return false; } @@ -357,13 +358,14 @@ class CodePushArtifactManager { .toIso8601String() .replaceAll(RegExp('[^0-9]'), '') .substring(0, 14); - final backupDir = - Directory('$engineCache/.fcp-stock-backup-$stamp'); + final backupDir = Directory('$engineCache/.fcp-stock-backup-$stamp'); backupDir.createSync(recursive: true); - Directory('${backupDir.path}/flutter_patched_sdk_product') - .createSync(recursive: true); - Directory('${backupDir.path}/ios-release/Flutter.framework') - .createSync(recursive: true); + Directory( + '${backupDir.path}/flutter_patched_sdk_product', + ).createSync(recursive: true); + Directory( + '${backupDir.path}/ios-release/Flutter.framework', + ).createSync(recursive: true); void backup(File src, String destRel) { if (!src.existsSync()) return; @@ -372,16 +374,16 @@ class CodePushArtifactManager { dest.writeAsBytesSync(src.readAsBytesSync()); } - final stockPlatform = - File('${productSdkDir.path}/platform_strong.dill'); - final stockVmOutline = - File('${productSdkDir.path}/vm_outline_strong.dill'); + final stockPlatform = File('${productSdkDir.path}/platform_strong.dill'); + final stockVmOutline = File('${productSdkDir.path}/vm_outline_strong.dill'); final stockFlutter = File('${frameworkDir.path}/Flutter'); final stockGenSnap = File('${iosReleaseDir.path}/gen_snapshot_arm64'); backup(stockPlatform, 'flutter_patched_sdk_product/platform_strong.dill'); - backup(stockVmOutline, - 'flutter_patched_sdk_product/vm_outline_strong.dill'); + backup( + stockVmOutline, + 'flutter_patched_sdk_product/vm_outline_strong.dill', + ); backup(stockFlutter, 'ios-release/Flutter.framework/Flutter'); backup(stockGenSnap, 'ios-release/gen_snapshot_arm64'); _logger.detail('Stock artifacts backed up to ${backupDir.path}'); @@ -403,10 +405,12 @@ class CodePushArtifactManager { // Extract xcframework to a temp dir and locate the ios-arm64 binary. final tempXcf = Directory.systemTemp.createTempSync('fcp-xcf-'); try { - final tarResult = Process.runSync( - 'tar', - ['-xzf', xcframeworkTar!.path, '-C', tempXcf.path], - ); + final tarResult = Process.runSync('tar', [ + '-xzf', + xcframeworkTar!.path, + '-C', + tempXcf.path, + ]); if (tarResult.exitCode != 0) { _logger.err( 'Failed to extract Flutter.xcframework.tar.gz: ${tarResult.stderr}', @@ -491,9 +495,7 @@ class CodePushArtifactManager { _logger.detail('Detected Flutter version: $flutterVersion'); if (isVersionCached(flutterVersion)) { - _logger.info( - 'Artifacts already cached for Flutter $flutterVersion.', - ); + _logger.info('Artifacts already cached for Flutter $flutterVersion.'); return flutterVersion; } @@ -583,8 +585,11 @@ class CodePushArtifactManager { targetSdk.deleteSync(recursive: true); } - final result = - Process.runSync('cp', ['-R', sourceSdk.path, targetSdk.path]); + final result = Process.runSync('cp', [ + '-R', + sourceSdk.path, + targetSdk.path, + ]); if (result.exitCode != 0) { _logger.err('Failed to copy Dart SDK: ${result.stderr}'); return false; @@ -593,8 +598,9 @@ class CodePushArtifactManager { final engineStamp = File('$targetFlutterRoot/bin/cache/engine.stamp'); if (engineStamp.existsSync()) { final hash = engineStamp.readAsStringSync().trim(); - File('$targetFlutterRoot/bin/cache/engine-dart-sdk.stamp') - .writeAsStringSync(hash); + File( + '$targetFlutterRoot/bin/cache/engine-dart-sdk.stamp', + ).writeAsStringSync(hash); } return true; diff --git a/lib/src/shared/codepush_build_service.dart b/lib/src/shared/codepush_build_service.dart index d308a1a..892b21e 100644 --- a/lib/src/shared/codepush_build_service.dart +++ b/lib/src/shared/codepush_build_service.dart @@ -179,9 +179,8 @@ class CodePushBuildService { return false; } - final isIos = platform == 'ios' && - artifactManager != null && - flutterVersion != null; + final isIos = + platform == 'ios' && artifactManager != null && flutterVersion != null; String? expectedGenSnapshotSha; String? expectedFrameworkSha; @@ -248,8 +247,7 @@ class CodePushBuildService { expectedFrameworkSha != null && sdkGenSnapshotPath != null && sdkFrameworkPath != null) { - final postBuildGenSnapshotSha = - _sha256OfFile(File(sdkGenSnapshotPath)); + final postBuildGenSnapshotSha = _sha256OfFile(File(sdkGenSnapshotPath)); final genSnapshotDrifted = postBuildGenSnapshotSha != expectedGenSnapshotSha; @@ -262,8 +260,7 @@ class CodePushBuildService { 'flutter build reset gen_snapshot to stock. ' 'Re-overlaying and rebuilding...', ); - final reOverlayOk = - await artifactManager.installOverlaysIntoFlutterSdk( + final reOverlayOk = await artifactManager.installOverlaysIntoFlutterSdk( flutterVersion: flutterVersion, platform: artifactManager.currentPlatform, ); @@ -401,9 +398,7 @@ class CodePushBuildService { 'build/app/intermediates/flutter/release/armeabi-v7a/app.so', 'build/app/outputs/flutter-apk/app-release.apk', ], - 'appbundle': [ - 'build/app/intermediates/flutter/release/app.so', - ], + 'appbundle': ['build/app/intermediates/flutter/release/app.so'], 'linux': [ 'build/linux/x64/release/bundle/lib/libapp.so', 'build/linux/arm64/release/bundle/lib/libapp.so', @@ -411,9 +406,7 @@ class CodePushBuildService { 'macos': [ 'build/macos/Build/Products/Release/Runner.app/Contents/Frameworks/App.framework/App', ], - 'windows': [ - 'build/windows/x64/runner/Release/app.so', - ], + 'windows': ['build/windows/x64/runner/Release/app.so'], }; final candidates = platformPaths[platform] ?? []; @@ -434,10 +427,7 @@ class CodePushBuildService { /// Verify the first bytes of a patch payload match the expected /// format for [platform]. Returns `null` on pass; a short error /// string on fail. - static String? validatePayloadMagic( - List payload, - String platform, - ) { + static String? validatePayloadMagic(List payload, String platform) { if (payload.length < 4) { return 'Patch payload is too small to be valid ' '(got ${payload.length} bytes).'; @@ -567,9 +557,7 @@ class CodePushBuildService { }) async { if (buildPlatform == 'ios') { if (flutterVersion == null || flutterVersion.isEmpty) { - _logger.err( - 'Flutter version is required to prepare an iOS fcp build.', - ); + _logger.err('Flutter version is required to prepare an iOS fcp build.'); return false; } final overlaysInstalled = diff --git a/lib/src/shared/ios_patch_entry_target.dart b/lib/src/shared/ios_patch_entry_target.dart index d7fa101..da59555 100644 --- a/lib/src/shared/ios_patch_entry_target.dart +++ b/lib/src/shared/ios_patch_entry_target.dart @@ -7,9 +7,7 @@ final RegExp _codePushPatchPattern = RegExp( multiLine: true, ); -List findCodePushPatchSourceCandidates({ - String libDirPath = 'lib', -}) { +List findCodePushPatchSourceCandidates({String libDirPath = 'lib'}) { final libDir = Directory(libDirPath); if (!libDir.existsSync()) return const []; @@ -61,7 +59,7 @@ String buildGeneratedIosPatchEntrypoint({ // This avoids cross-library DirectCall entirely but produces a // new library (no overlap, no function swap). if (patchSourcePath != null) { - final sourceFile = File(patchSourcePath); + final sourceFile = File(patchSourcePath); if (sourceFile.existsSync()) { var source = sourceFile.readAsStringSync(); @@ -70,7 +68,8 @@ String buildGeneratedIosPatchEntrypoint({ // imports like `import 'foo.dart'` need to become // `import 'screens/foo.dart'` so they resolve from lib/. final sourceDir = _normalizePath( - sourceFile.parent.path.replaceFirst(RegExp(r'^lib/?'), '')); + sourceFile.parent.path.replaceFirst(RegExp(r'^lib/?'), ''), + ); if (sourceDir.isNotEmpty) { source = source.replaceAllMapped( RegExp(r'''(import\s+['"])(?!dart:|package:)([^'"]+['"])'''), diff --git a/test/src/shared/codepush_archive_service_test.dart b/test/src/shared/codepush_archive_service_test.dart index 9c08b35..54a3d6c 100644 --- a/test/src/shared/codepush_archive_service_test.dart +++ b/test/src/shared/codepush_archive_service_test.dart @@ -14,10 +14,7 @@ void main() { setUp(() { projectDir = Directory.systemTemp.createTempSync('fcp-archive-test-'); logger = Logger(level: Level.quiet); - service = CodePushArchiveService( - logger: logger, - projectDir: projectDir, - ); + service = CodePushArchiveService(logger: logger, projectDir: projectDir); }); tearDown(() { @@ -34,15 +31,21 @@ void main() { final baselineApp = Directory( '${projectDir.path}/build/codepush/baseline/Runner.app', ); - Directory('${baselineApp.path}/Frameworks/Flutter.framework') - .createSync(recursive: true); - Directory('${baselineApp.path}/Frameworks/App.framework') - .createSync(recursive: true); - File('${baselineApp.path}/Frameworks/Flutter.framework/Flutter') - .writeAsStringSync(flutterFrameworkContents); - File('${baselineApp.path}/Frameworks/App.framework/App') - .writeAsStringSync(appFrameworkContents); - File('${baselineApp.path}/Runner').writeAsStringSync(runnerBinaryContents); + Directory( + '${baselineApp.path}/Frameworks/Flutter.framework', + ).createSync(recursive: true); + Directory( + '${baselineApp.path}/Frameworks/App.framework', + ).createSync(recursive: true); + File( + '${baselineApp.path}/Frameworks/Flutter.framework/Flutter', + ).writeAsStringSync(flutterFrameworkContents); + File( + '${baselineApp.path}/Frameworks/App.framework/App', + ).writeAsStringSync(appFrameworkContents); + File( + '${baselineApp.path}/Runner', + ).writeAsStringSync(runnerBinaryContents); File('${baselineApp.path}/Info.plist').writeAsStringSync('plist'); } @@ -55,10 +58,12 @@ void main() { } void initGitRepo() { - final result = Process.runSync( - 'git', - ['-C', projectDir.path, 'init', '-q'], - ); + final result = Process.runSync('git', [ + '-C', + projectDir.path, + 'init', + '-q', + ]); if (result.exitCode != 0) { fail('git init failed: ${result.stderr}'); } @@ -87,8 +92,7 @@ void main() { ); expect(ok, isTrue); - final releaseDir = - Directory('${projectDir.path}/.fcp-archive/rel-2'); + final releaseDir = Directory('${projectDir.path}/.fcp-archive/rel-2'); expect(releaseDir.existsSync(), isTrue); expect( File('${releaseDir.path}/Runner.app/Info.plist').existsSync(), @@ -127,8 +131,7 @@ void main() { ); expect(ok, isTrue); - final releaseDir = - Directory('${projectDir.path}/.fcp-archive/rel-3'); + final releaseDir = Directory('${projectDir.path}/.fcp-archive/rel-3'); expect( Directory('${releaseDir.path}/Runner.app.dSYM').existsSync(), isTrue, @@ -154,28 +157,25 @@ void main() { ); }); - test( - 'appends .fcp-archive/ to .git/info/exclude in a git repo', - () { - initGitRepo(); - writeBaselineApp(); - - service.archiveIosRelease( - releaseId: 'rel-5', - baselineId: 'base-5', - fcpVersion: '0.0.0', - ); - - final excludeFile = File('${projectDir.path}/.git/info/exclude'); - expect(excludeFile.existsSync(), isTrue); - final lines = excludeFile - .readAsStringSync() - .split('\n') - .map((l) => l.trim()) - .toList(); - expect(lines.contains('.fcp-archive/'), isTrue); - }, - ); + test('appends .fcp-archive/ to .git/info/exclude in a git repo', () { + initGitRepo(); + writeBaselineApp(); + + service.archiveIosRelease( + releaseId: 'rel-5', + baselineId: 'base-5', + fcpVersion: '0.0.0', + ); + + final excludeFile = File('${projectDir.path}/.git/info/exclude'); + expect(excludeFile.existsSync(), isTrue); + final lines = excludeFile + .readAsStringSync() + .split('\n') + .map((l) => l.trim()) + .toList(); + expect(lines.contains('.fcp-archive/'), isTrue); + }); test('does not duplicate the exclude entry on re-runs', () { initGitRepo(); @@ -192,9 +192,11 @@ void main() { fcpVersion: '0.0.0', ); - final excludeContents = - File('${projectDir.path}/.git/info/exclude').readAsStringSync(); - final occurrences = '\n$excludeContents\n'.split('\n.fcp-archive/').length - 1; + final excludeContents = File( + '${projectDir.path}/.git/info/exclude', + ).readAsStringSync(); + final occurrences = + '\n$excludeContents\n'.split('\n.fcp-archive/').length - 1; expect(occurrences, 1); }); @@ -209,10 +211,7 @@ void main() { expect(ok, isTrue); // No .git directory was created. - expect( - Directory('${projectDir.path}/.git').existsSync(), - isFalse, - ); + expect(Directory('${projectDir.path}/.git').existsSync(), isFalse); }); test('overwrites an existing release archive on re-run', () {