Skip to content

Commit 1115475

Browse files
buenaflorclaudeCopilot
authored
ci: Automate JNI branch sync on release (#3593)
* ci: Automate JNI branch sync on release Add a workflow that triggers on release published to automatically sync the deps/jni-0.15.x branch. It merges the release tag, regenerates JNI bindings via a safe fallback script, opens a PR, and updates the version tracker table on issue #3373. The safe-regenerate script tries the normal path first (flutter build apk + jnigen). If that fails due to binding incompatibility, it falls back to compiling only the Java/Kotlin plugin code via Gradle and creating a stub libs.jar for jnigen — bypassing Dart compilation. Refs GH-3373 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: Use push trigger for version tracker and fix job conditions Switch update-tracker from pull_request:closed to push trigger on deps/jni-0.15.x so it fires on any merge (PR or direct push) and correctly uses the merge commit SHA. Guard sync job with explicit event_name == 'release' check to prevent it running on push events. Refs GH-3373 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update .github/workflows/sync-jni-branch.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * ci(sync-jni): Harden workflow security and scope permissions Pass github.event.release.tag_name through env instead of inline ${{ }} interpolation to prevent script injection. Add tag format validation. Move all context properties out of run blocks into env. Scope permissions per-job: sync gets contents/pull-requests/issues write, update-tracker gets only issues write. Also read version from pubspec.yaml instead of parsing commit messages, making version detection resilient to squash merges. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): Create failure issue on merge conflicts in JNI sync The merge step lacked continue-on-error, so merge conflicts halted the workflow before the issue creation step could run. Now the merge step allows continuation on failure, downstream steps are gated on merge success, and the failure issue fires for either merge or regeneration failures with a "Failed step" indicator. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci(sync-jni): Remove update-tracker job and clean up triggers Remove the update-tracker job and its push trigger since version tracking on the issue is no longer needed. Restore safe concurrency settings (cancel-in-progress: false) to prevent cancelling in-flight syncs. Simplify job condition to just the prerelease check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(ci): Remove unused env vars from failure step Remove REPO and REGENERATE_OUTCOME env vars that were declared but never referenced in the run script. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 40751bf commit 1115475

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# This workflow is only needed for the maintenance of the jni 0.15.x branch.
2+
# See https://github.com/getsentry/sentry-dart/issues/3373
3+
# This can be removed in v10 when we can support jni 0.15.x in the main branch.
4+
5+
name: Sync JNI Branch
6+
7+
on:
8+
release:
9+
types: [published]
10+
11+
env:
12+
JNI_BRANCH: deps/jni-0.15.x
13+
14+
concurrency:
15+
group: sync-jni-branch
16+
cancel-in-progress: false
17+
18+
jobs:
19+
sync:
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: write
23+
pull-requests: write
24+
issues: write
25+
timeout-minutes: 30
26+
if: ${{ !github.event.release.prerelease }}
27+
28+
steps:
29+
- name: Get auth token
30+
id: token
31+
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
32+
with:
33+
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
34+
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
35+
36+
- name: Determine release tag
37+
id: release
38+
env:
39+
RELEASE_TAG: ${{ github.event.release.tag_name }}
40+
run: |
41+
# Validate tag format to prevent script injection
42+
if [[ ! "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then
43+
echo "::error::Invalid tag format: $RELEASE_TAG"
44+
exit 1
45+
fi
46+
echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
47+
echo "Release tag: $RELEASE_TAG"
48+
49+
- name: Checkout
50+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
51+
with:
52+
token: ${{ steps.token.outputs.token }}
53+
fetch-depth: 0
54+
ref: ${{ env.JNI_BRANCH }}
55+
56+
- name: Setup Java
57+
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
58+
with:
59+
distribution: 'temurin'
60+
java-version: '17'
61+
62+
- name: Setup Flutter
63+
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # pin@v2.21.0
64+
65+
- name: Configure git
66+
run: |
67+
git config user.name 'Sentry Github Bot'
68+
git config user.email 'bot+github-bot@sentry.io'
69+
70+
- name: Create sync branch
71+
id: branch
72+
env:
73+
TAG: ${{ steps.release.outputs.tag }}
74+
run: |
75+
SYNC_BRANCH="sync/jni-$TAG"
76+
git checkout -b "$SYNC_BRANCH"
77+
echo "name=$SYNC_BRANCH" >> $GITHUB_OUTPUT
78+
79+
- name: Merge release tag
80+
id: merge
81+
continue-on-error: true
82+
env:
83+
TAG: ${{ steps.release.outputs.tag }}
84+
BRANCH: ${{ env.JNI_BRANCH }}
85+
run: |
86+
git merge "$TAG" --no-edit \
87+
-m "Merge release $TAG into $BRANCH"
88+
89+
- name: Regenerate JNI bindings
90+
id: regenerate
91+
if: steps.merge.outcome == 'success'
92+
continue-on-error: true
93+
working-directory: packages/flutter
94+
run: |
95+
scripts/safe-regenerate-jni.sh 2>&1 | tee /tmp/jni-regen.log
96+
97+
- name: Commit regenerated bindings
98+
if: steps.merge.outcome == 'success' && steps.regenerate.outcome == 'success'
99+
env:
100+
TAG: ${{ steps.release.outputs.tag }}
101+
run: |
102+
git add packages/flutter/lib/src/native/java/binding.dart
103+
if ! git diff --cached --quiet; then
104+
git commit -m "chore: regenerate JNI bindings for $TAG"
105+
fi
106+
107+
- name: Push and create PR
108+
if: steps.merge.outcome == 'success' && steps.regenerate.outcome == 'success'
109+
env:
110+
GH_TOKEN: ${{ steps.token.outputs.token }}
111+
TAG: ${{ steps.release.outputs.tag }}
112+
BRANCH: ${{ env.JNI_BRANCH }}
113+
SYNC_BRANCH: ${{ steps.branch.outputs.name }}
114+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
115+
run: |
116+
git push -u origin "$SYNC_BRANCH"
117+
{
118+
echo "## Summary"
119+
echo "Automated sync of the JNI compatibility branch with release \`$TAG\`."
120+
echo "This PR merges the release tag into \`$BRANCH\` and regenerates JNI bindings."
121+
echo ""
122+
echo "## Checklist"
123+
echo "- [ ] Verify JNI bindings compile successfully"
124+
echo "- [ ] Verify tests pass"
125+
echo ""
126+
echo "> This PR was auto-generated by the [sync-jni-branch]($RUN_URL) workflow."
127+
} > /tmp/pr-body.md
128+
gh pr create \
129+
--base "$BRANCH" \
130+
--title "Sync JNI branch with release $TAG" \
131+
--body-file /tmp/pr-body.md
132+
133+
- name: Create issue on failure
134+
if: steps.merge.outcome == 'failure' || steps.regenerate.outcome == 'failure'
135+
env:
136+
GH_TOKEN: ${{ steps.token.outputs.token }}
137+
TAG: ${{ steps.release.outputs.tag }}
138+
BRANCH: ${{ env.JNI_BRANCH }}
139+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
140+
MERGE_OUTCOME: ${{ steps.merge.outcome }}
141+
run: |
142+
if [ "$MERGE_OUTCOME" = "failure" ]; then
143+
FAILED_STEP="Merge release tag"
144+
else
145+
FAILED_STEP="Regenerate JNI bindings"
146+
fi
147+
LOG_TAIL="$(tail -100 /tmp/jni-regen.log 2>/dev/null || echo 'No log output available')"
148+
{
149+
echo "## JNI Branch Sync Failure"
150+
echo "The automated sync of the JNI compatibility branch failed for release \`$TAG\`."
151+
echo ""
152+
echo "**Failed step:** $FAILED_STEP"
153+
echo "**Workflow run:** $RUN_URL"
154+
echo "**Target branch:** \`$BRANCH\`"
155+
echo ""
156+
echo "## Error Logs (last 100 lines)"
157+
echo '```'
158+
echo "$LOG_TAIL"
159+
echo '```'
160+
echo ""
161+
echo "## Manual Remediation"
162+
echo "1. Checkout the JNI branch: \`git checkout $BRANCH\`"
163+
echo "2. Merge the release tag: \`git merge $TAG\`"
164+
echo "3. Run: \`cd packages/flutter && scripts/safe-regenerate-jni.sh\`"
165+
echo "4. Resolve any issues manually"
166+
echo "5. Commit and push the changes"
167+
} > /tmp/issue-body.md
168+
gh issue create \
169+
--title "JNI branch sync failed for release $TAG" \
170+
--body-file /tmp/issue-body.md
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Safe JNI binding regeneration script.
5+
#
6+
# Tries the normal regeneration first (flutter build apk + jnigen).
7+
# If that fails (e.g. binding.dart is incompatible with a new jni version),
8+
# falls back to compiling only the Java/Kotlin plugin code via Gradle,
9+
# creating a stub libs.jar, and running jnigen directly — bypassing
10+
# Dart compilation entirely.
11+
#
12+
# This script is only needed for the maintenance of the jni 0.15.x branch.
13+
# See https://github.com/getsentry/sentry-dart/issues/3373
14+
# This can be removed in v10 when we can support jni 0.15.x in the main branch.
15+
16+
cd "$(dirname "$0")/../"
17+
18+
binding_path="lib/src/native/java/binding.dart"
19+
20+
# Use fvm if available, otherwise bare commands.
21+
if command -v fvm &> /dev/null; then
22+
FLUTTER="fvm flutter"
23+
DART="fvm dart"
24+
else
25+
FLUTTER="flutter"
26+
DART="dart"
27+
fi
28+
29+
echo "=== Attempting normal JNI binding regeneration ==="
30+
31+
# The normal path: build APK (compiles Dart + Java), then run jnigen.
32+
set +e
33+
(
34+
cd example
35+
$FLUTTER build apk
36+
)
37+
normal_build=$?
38+
set -e
39+
40+
if [ $normal_build -eq 0 ]; then
41+
$DART run jnigen --config ffi-jni.yaml
42+
$DART format "$binding_path"
43+
echo "=== Normal regeneration succeeded ==="
44+
exit 0
45+
fi
46+
47+
echo ""
48+
echo "=== Normal regeneration failed (exit code $normal_build), attempting fallback ==="
49+
echo ""
50+
51+
# The normal path failed, likely because the existing binding.dart
52+
# doesn't compile with the current jni package version.
53+
#
54+
# Fallback strategy:
55+
# 1. Clean Gradle build artifacts
56+
# 2. Compile only the sentry_flutter plugin's Java/Kotlin code via Gradle
57+
# (this does NOT require Dart to compile)
58+
# 3. Create a stub libs.jar so jnigen's Gradle classpath resolution succeeds
59+
# (libs.jar normally contains compiled Dart code from flutter build apk,
60+
# but jnigen only needs the Java classes, not the Dart code)
61+
# 4. Run jnigen directly to regenerate binding.dart
62+
# 5. Verify the result by building the full APK
63+
64+
echo "Step 1: Cleaning Gradle build artifacts..."
65+
cd example/android
66+
./gradlew clean
67+
cd ../..
68+
69+
echo "Step 2: Compiling sentry_flutter plugin Java/Kotlin code..."
70+
cd example/android
71+
./gradlew :sentry_flutter:bundleLibCompileToJarRelease
72+
cd ../..
73+
74+
echo "Step 3: Creating stub libs.jar..."
75+
# jnigen's Gradle classpath includes libs.jar (normally produced by flutter build apk).
76+
# It must be a valid jar but doesn't need to contain Dart compiled classes —
77+
# jnigen only reads Java classes from the Sentry Android SDK jars and Maven caches.
78+
stub_dir=$(mktemp -d)
79+
mkdir -p example/build/app/intermediates/flutter/release
80+
jar cf example/build/app/intermediates/flutter/release/libs.jar -C "$stub_dir" .
81+
rm -rf "$stub_dir"
82+
83+
echo "Step 4: Running jnigen..."
84+
$DART run jnigen --config ffi-jni.yaml
85+
86+
echo "Step 5: Formatting generated binding..."
87+
$DART format "$binding_path"
88+
89+
echo "Step 6: Verifying by building example APK..."
90+
set +e
91+
(
92+
cd example
93+
$FLUTTER build apk
94+
)
95+
verify_result=$?
96+
set -e
97+
98+
if [ $verify_result -ne 0 ]; then
99+
echo ""
100+
echo "=== ERROR: Regenerated bindings do not compile ==="
101+
echo "=== Manual intervention is needed ==="
102+
exit 1
103+
fi
104+
105+
echo ""
106+
echo "=== Fallback regeneration succeeded ==="
107+
exit 0

0 commit comments

Comments
 (0)