Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
fonkamloic marked this conversation as resolved.
Comment thread
fonkamloic marked this conversation as resolved.
Comment thread
fonkamloic marked this conversation as resolved.
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}

Comment thread
fonkamloic marked this conversation as resolved.
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:*)"
385 changes: 180 additions & 205 deletions CHANGELOG.md

Large diffs are not rendered by default.

110 changes: 90 additions & 20 deletions lib/src/commands/codepush_commands/_codepush_init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:flutter_compile/src/shared/codepush_build_service.dart';
import 'package:flutter_compile/src/shared/codepush_client.dart';
import 'package:flutter_compile/src/shared/functions.dart';
import 'package:mason_logger/mason_logger.dart';

class CodePushInitSubCommand extends Command<int> {
Expand Down Expand Up @@ -181,12 +182,12 @@ class CodePushInitSubCommand extends Command<int> {
' override fun onCreate() {\n'
' try {\n'
' val dest = File(filesDir, "codepush.yaml")\n'
' if (!dest.exists()) {\n'
' assets.open("codepush.yaml").use { i ->\n'
' dest.outputStream().use { o -> i.copyTo(o) }\n'
' }\n'
' assets.open("codepush.yaml").use { i ->\n'
' dest.outputStream().use { o -> i.copyTo(o) }\n'
' }\n'
' } catch (_: Exception) {}\n'
' } catch (e: Exception) {\n'
' android.util.Log.e("CodePushApp", "copy failed", e)\n'
' }\n'
' super.onCreate()\n'
' }\n'
' }\n'
Expand Down Expand Up @@ -239,12 +240,27 @@ class CodePushInitSubCommand extends Command<int> {

final progress = _logger.progress('Setting up Android');

// 1. Create codepush.yaml in assets
// 1. Create codepush.yaml in assets. Include the signing public key
// when one exists so devices verify patch signatures (parity with the
Comment thread
fonkamloic marked this conversation as resolved.
// FLTCodePushPublicKey Info.plist entry on iOS).
final assetsDir = Directory('${androidDir.path}/assets');
if (!assetsDir.existsSync()) assetsDir.createSync(recursive: true);
final configFile = File('${assetsDir.path}/codepush.yaml');
var keyBlock = _publicKeyYamlBlock();
if (keyBlock.isEmpty && configFile.existsSync()) {
// No local key (CI, different machine, post-rotation) — keep a key
Comment thread
fonkamloic marked this conversation as resolved.
// previously injected by `fcp codepush keys register` rather than
// silently disabling signature verification on devices.
final match =
kPublicKeyYamlBlockPattern.firstMatch(configFile.readAsStringSync());
if (match != null) {
keyBlock = match.group(0)!;
if (!keyBlock.endsWith('\n')) keyBlock = '$keyBlock\n';
_logger.detail('Preserved existing public_key in codepush.yaml');
}
}
configFile.writeAsStringSync(
'enabled: true\nrelease_version: "$version"\n',
'enabled: true\nrelease_version: "$version"\n$keyBlock',
);

// 2. Find the package name and source directory
Expand Down Expand Up @@ -295,6 +311,27 @@ class CodePushInitSubCommand extends Command<int> {
);
if (!kotlinDir.existsSync()) kotlinDir.createSync(recursive: true);
final codePushAppFile = File('${kotlinDir.path}/CodePushApp.kt');
// The copy must run on every launch: the asset is the source of truth
// and changes with each app update, while a copy guarded by exists()
// would stay frozen at whatever the first install shipped.
const oldCopyBlock = '''
try {
val dest = File(filesDir, "codepush.yaml")
if (!dest.exists()) {
assets.open("codepush.yaml").use { input ->
dest.outputStream().use { output -> input.copyTo(output) }
}
}
} catch (_: Exception) {}''';
const newCopyBlock = '''
try {
val dest = File(filesDir, "codepush.yaml")
assets.open("codepush.yaml").use { input ->
dest.outputStream().use { output -> input.copyTo(output) }
}
} catch (e: Exception) {
android.util.Log.e("CodePushApp", "Failed to copy codepush.yaml", e)
}''';
if (!codePushAppFile.existsSync()) {
codePushAppFile.writeAsStringSync('''
package $packageName
Expand All @@ -305,18 +342,27 @@ import java.io.File
class CodePushApp : FlutterApplication() {
override fun onCreate() {
// Copy codepush.yaml from assets to files dir before Flutter engine init.
try {
val dest = File(filesDir, "codepush.yaml")
if (!dest.exists()) {
assets.open("codepush.yaml").use { input ->
dest.outputStream().use { output -> input.copyTo(output) }
}
}
} catch (_: Exception) {}
$newCopyBlock
super.onCreate()
}
}
''');
} else {
// Upgrade a CodePushApp.kt generated by an older CLI version, which
// skipped the copy when the file already existed.
final existing = codePushAppFile.readAsStringSync();
if (existing.contains(oldCopyBlock)) {
Comment thread
fonkamloic marked this conversation as resolved.
codePushAppFile.writeAsStringSync(
existing.replaceFirst(oldCopyBlock, newCopyBlock));
_logger.info(
' Updated: CodePushApp.kt (config now refreshes on every launch)');
} else if (!existing.contains(newCopyBlock)) {
_logger.warn(
' CodePushApp.kt was not upgraded automatically (the file has '
'been modified). Make sure the codepush.yaml copy in onCreate() '
'runs on every launch — remove any exists-check around it.',
);
}
Comment thread
fonkamloic marked this conversation as resolved.
}

// 4. Update AndroidManifest.xml to use CodePushApp
Expand All @@ -340,12 +386,12 @@ class CodePushApp : FlutterApplication() {
' Add this to its onCreate() BEFORE super.onCreate():\n'
' try {\n'
' val dest = java.io.File(filesDir, "codepush.yaml")\n'
' if (!dest.exists()) {\n'
' assets.open("codepush.yaml").use { i ->\n'
' dest.outputStream().use { o -> i.copyTo(o) }\n'
' }\n'
' assets.open("codepush.yaml").use { i ->\n'
' dest.outputStream().use { o -> i.copyTo(o) }\n'
' }\n'
' } catch (_: Exception) {}',
' } catch (e: Exception) {\n'
' android.util.Log.e("CodePushApp", "Failed to copy codepush.yaml", e)\n'
' }',
);
}

Expand All @@ -355,6 +401,30 @@ class CodePushApp : FlutterApplication() {
_logger.info(' Updated: AndroidManifest.xml');
}

/// Returns a `public_key: |` YAML block for codepush.yaml when a local
/// signing public key exists, or an empty string otherwise. With a key
/// in the config, devices require a valid patch signature; without one,
Comment thread
fonkamloic marked this conversation as resolved.
/// only integrity checks run.
String _publicKeyYamlBlock() {
final publicKeyFile =
File('${F.homeDir()}/.flutter_codepush/codepush_public.pem');
if (!publicKeyFile.existsSync()) return '';
Comment thread
fonkamloic marked this conversation as resolved.
final String pem;
try {
pem = publicKeyFile.readAsStringSync().trim();
} on FileSystemException catch (e) {
_logger.warn(
'Could not read ${publicKeyFile.path}: $e — continuing without '
'embedding the public key.',
);
return '';
}
if (pem.isEmpty) return '';
final indented =
pem.split('\n').map((line) => ' ${line.trim()}').join('\n');
return 'public_key: |\n$indented\n';
}

// ── iOS setup ─────────────────────────────────────────────────

void _setupIos(String version) {
Expand Down
38 changes: 38 additions & 0 deletions lib/src/commands/codepush_commands/_codepush_keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:flutter_compile/src/shared/codepush_build_service.dart';
import 'package:flutter_compile/src/shared/functions.dart';
import 'package:flutter_compile/src/shared/codepush_client.dart';
import 'package:mason_logger/mason_logger.dart';

Expand Down Expand Up @@ -242,6 +243,8 @@ class _KeysRegisterCommand extends Command<int> {
'be rejected with HTTP 403.',
);

_maybeAddPublicKeyToAndroidConfig(publicKeyPem);

return ExitCode.success.code;
} catch (e) {
progress.fail('Failed: $e');
Expand All @@ -250,6 +253,41 @@ class _KeysRegisterCommand extends Command<int> {
client.close();
}
}

/// Writes the public key into android/app/src/main/assets/codepush.yaml
/// when the project has Android code push set up, replacing any previous
/// key so rotation takes effect on devices (parity with
/// FLTCodePushPublicKey on iOS).
void _maybeAddPublicKeyToAndroidConfig(String publicKeyPem) {
if (publicKeyPem.trim().isEmpty) return;
const yamlPath = 'android/app/src/main/assets/codepush.yaml';
try {
final yamlFile = File(yamlPath);
if (!yamlFile.existsSync()) return;
var content = yamlFile.readAsStringSync();
final indented =
publicKeyPem.split('\n').map((line) => ' ${line.trim()}').join('\n');
final block = 'public_key: |\n$indented\n';
if (content.contains(block)) return; // already up to date
final hadKey = kPublicKeyYamlBlockPattern.hasMatch(content);
Comment thread
fonkamloic marked this conversation as resolved.
content = content.replaceAll(kPublicKeyYamlBlockPattern, '');
if (content.isNotEmpty && !content.endsWith('\n')) content += '\n';
yamlFile.writeAsStringSync('$content$block');
_logger.info(
hadKey
? 'Updated the public key in $yamlPath. Devices verify against '
'the new key from your next release build.'
: 'Added the public key to $yamlPath. Devices will verify patch '
'signatures from your next release build.',
);
} on FileSystemException catch (e) {
_logger.warn(
'The key was registered on the server, but updating $yamlPath '
'failed: $e. Re-run `fcp codepush keys register` from the project '
'root to embed it.',
);
}
}
}

String _defaultKeyDir() {
Expand Down
5 changes: 5 additions & 0 deletions lib/src/shared/functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import 'package:flutter_compile/src/shared/exception.dart';
import 'package:flutter_compile/src/shared/extension.dart';
import 'package:mason_logger/mason_logger.dart';

/// Matches a `public_key:` entry in an Android codepush.yaml, including a
/// `|` block scalar's indented continuation lines.
final RegExp kPublicKeyYamlBlockPattern =
RegExp(r'public_key:[^\n]*\n(?:[ \t]+[^\n]*(?:\n|$))*');

Comment thread
fonkamloic marked this conversation as resolved.
class MigrateResult {
final int blocksMoved;
final bool sourceLineAdded;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/version.dart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
const packageVersion = '0.19.14';
const packageVersion = '0.19.15-rc.9';
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_compile
description: A CLI that automates Flutter framework, DevTools, and engine contributor environment setup — depot_tools, gclient sync, GN flags, and ninja builds.
version: 0.19.14
version: 0.19.15-rc.9
repository: https://github.com/FlutterPlaza/flutter_compile
homepage: https://www.flutterplaza.com/
issue_tracker: https://github.com/FlutterPlaza/flutter_compile/issues
Expand Down
Loading