Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
247 changes: 247 additions & 0 deletions .github/workflows/sync-jni-branch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# This workflow is only needed for the maintenance of the jni 0.15.x branch.
# See https://github.com/getsentry/sentry-dart/issues/3373
# This can be removed in v10 when we can support jni 0.15.x in the main branch.

name: Sync JNI Branch

on:
release:
types: [published]
push:
branches:
- deps/jni-0.15.x

env:
JNI_BRANCH: deps/jni-0.15.x

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
timeout-minutes: 30
if: >-
github.event_name == 'release' &&
!github.event.release.prerelease

steps:
- name: Get auth token
id: token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}

- name: Determine release tag
id: release
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
# Validate tag format to prevent script injection
if [[ ! "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then
echo "::error::Invalid tag format: $RELEASE_TAG"
exit 1
fi
echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
echo "Release tag: $RELEASE_TAG"

- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ steps.token.outputs.token }}
fetch-depth: 0
ref: ${{ env.JNI_BRANCH }}

- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Flutter
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # pin@v2.21.0

- name: Configure git
run: |
git config user.name 'Sentry Github Bot'
git config user.email 'bot+github-bot@sentry.io'

- name: Create sync branch
id: branch
env:
TAG: ${{ steps.release.outputs.tag }}
run: |
SYNC_BRANCH="sync/jni-$TAG"
git checkout -b "$SYNC_BRANCH"
echo "name=$SYNC_BRANCH" >> $GITHUB_OUTPUT

- name: Merge release tag
id: merge
continue-on-error: true
env:
TAG: ${{ steps.release.outputs.tag }}
BRANCH: ${{ env.JNI_BRANCH }}
run: |
git merge "$TAG" --no-edit \
-m "Merge release $TAG into $BRANCH"

- name: Regenerate JNI bindings
id: regenerate
if: steps.merge.outcome == 'success'
continue-on-error: true
working-directory: packages/flutter
run: |
scripts/safe-regenerate-jni.sh 2>&1 | tee /tmp/jni-regen.log
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember you mentioning that some files need to be deleted or commented out before re-generation, is this still true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safe-regenerate-jni.sh handles this now - it's not strictly needed anymore since the script will create a stub so it still compiles


- name: Commit regenerated bindings
if: steps.merge.outcome == 'success' && steps.regenerate.outcome == 'success'
env:
TAG: ${{ steps.release.outputs.tag }}
run: |
git add packages/flutter/lib/src/native/java/binding.dart
if ! git diff --cached --quiet; then
git commit -m "chore: regenerate JNI bindings for $TAG"
fi

- name: Push and create PR
if: steps.merge.outcome == 'success' && steps.regenerate.outcome == 'success'
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
TAG: ${{ steps.release.outputs.tag }}
BRANCH: ${{ env.JNI_BRANCH }}
SYNC_BRANCH: ${{ steps.branch.outputs.name }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
git push -u origin "$SYNC_BRANCH"
{
echo "## Summary"
echo "Automated sync of the JNI compatibility branch with release \`$TAG\`."
echo "This PR merges the release tag into \`$BRANCH\` and regenerates JNI bindings."
echo ""
echo "## Checklist"
echo "- [ ] Verify JNI bindings compile successfully"
echo "- [ ] Verify tests pass"
echo ""
echo "> This PR was auto-generated by the [sync-jni-branch]($RUN_URL) workflow."
} > /tmp/pr-body.md
gh pr create \
--base "$BRANCH" \
--title "Sync JNI branch with release $TAG" \
--body-file /tmp/pr-body.md

- name: Create issue on failure
if: steps.merge.outcome == 'failure' || steps.regenerate.outcome == 'failure'
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
TAG: ${{ steps.release.outputs.tag }}
BRANCH: ${{ env.JNI_BRANCH }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REPO: ${{ github.repository }}
MERGE_OUTCOME: ${{ steps.merge.outcome }}
REGENERATE_OUTCOME: ${{ steps.regenerate.outcome }}
run: |
if [ "$MERGE_OUTCOME" = "failure" ]; then
FAILED_STEP="Merge release tag"
else
FAILED_STEP="Regenerate JNI bindings"
fi
LOG_TAIL="$(tail -100 /tmp/jni-regen.log 2>/dev/null || echo 'No log output available')"
{
echo "## JNI Branch Sync Failure"
echo "The automated sync of the JNI compatibility branch failed for release \`$TAG\`."
echo ""
echo "**Failed step:** $FAILED_STEP"
echo "**Workflow run:** $RUN_URL"
echo "**Target branch:** \`$BRANCH\`"
echo ""
echo "## Error Logs (last 100 lines)"
echo '```'
echo "$LOG_TAIL"
echo '```'
echo ""
echo "## Manual Remediation"
echo "1. Checkout the JNI branch: \`git checkout $BRANCH\`"
echo "2. Merge the release tag: \`git merge $TAG\`"
echo "3. Run: \`cd packages/flutter && scripts/safe-regenerate-jni.sh\`"
echo "4. Resolve any issues manually"
echo "5. Commit and push the changes"
} > /tmp/issue-body.md
gh issue create \
--title "JNI branch sync failed for release $TAG" \
--body-file /tmp/issue-body.md

update-tracker:
runs-on: ubuntu-latest
permissions:
issues: write
if: github.event_name == 'push'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 10

- name: Extract version from pubspec
id: version
run: |
TAG="$(sed -n 's/^version: *//p' packages/dart/pubspec.yaml)"
if [ -z "$TAG" ]; then
echo "Could not read version from pubspec.yaml"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "skip=false" >> $GITHUB_OUTPUT
echo "Found version: $TAG"
fi

- name: Get auth token
if: steps.version.outputs.skip != 'true'
id: token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}

- name: Update version tracker on issue
if: steps.version.outputs.skip != 'true'
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
TAG: ${{ steps.version.outputs.tag }}
COMMIT_SHA: ${{ github.sha }}
REPO: ${{ github.repository }}
run: |
SHORT_SHA="${COMMIT_SHA:0:7}"
DATE="$(date -u +%Y-%m-%d)"
NEW_ROW="| \`$TAG\` | [\`$SHORT_SHA\`](https://github.com/$REPO/commit/$COMMIT_SHA) | $DATE |"

# Fetch current issue body
gh issue view 3373 --repo "$REPO" --json body --jq .body > /tmp/issue-body.txt

# Check if the version tracking table already exists
if grep -q "<!-- jni-version-tracker -->" /tmp/issue-body.txt; then
# Insert new row after the table header separator (|---|---|---|)
awk -v row="$NEW_ROW" \
'/^\|[-[:space:]|]+\|$/ && found { print; print row; found=0; next }
/<!-- jni-version-tracker -->/ { found=1 }
{ print }' /tmp/issue-body.txt > /tmp/issue-body-updated.txt
else
# Append a new table to the issue body
cp /tmp/issue-body.txt /tmp/issue-body-updated.txt
{
echo ""
echo "<!-- jni-version-tracker -->"
echo "## Version Tracking"
echo ""
echo "| Version | Commit | Date |"
echo "|---------|--------|------|"
echo "$NEW_ROW"
} >> /tmp/issue-body-updated.txt
fi

gh issue edit 3373 --repo "$REPO" --body-file /tmp/issue-body-updated.txt
106 changes: 106 additions & 0 deletions packages/flutter/scripts/safe-regenerate-jni.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
set -euo pipefail

# Safe JNI binding regeneration script.
#
# Tries the normal regeneration first (flutter build apk + jnigen).
# If that fails (e.g. binding.dart is incompatible with a new jni version),
# falls back to compiling only the Java/Kotlin plugin code via Gradle,
# creating a stub libs.jar, and running jnigen directly — bypassing
# Dart compilation entirely.
#
# This script is only needed for the maintenance of the jni 0.15.x branch.
# See https://github.com/getsentry/sentry-dart/issues/3373

cd "$(dirname "$0")/../"

binding_path="lib/src/native/java/binding.dart"

# Use fvm if available, otherwise bare commands.
if command -v fvm &> /dev/null; then
FLUTTER="fvm flutter"
DART="fvm dart"
else
FLUTTER="flutter"
DART="dart"
fi

echo "=== Attempting normal JNI binding regeneration ==="

# The normal path: build APK (compiles Dart + Java), then run jnigen.
set +e
(
cd example
$FLUTTER build apk
)
normal_build=$?
set -e

if [ $normal_build -eq 0 ]; then
$DART run jnigen --config ffi-jni.yaml
$DART format "$binding_path"
echo "=== Normal regeneration succeeded ==="
exit 0
fi

echo ""
echo "=== Normal regeneration failed (exit code $normal_build), attempting fallback ==="
echo ""

# The normal path failed, likely because the existing binding.dart
# doesn't compile with the current jni package version.
#
# Fallback strategy:
# 1. Clean Gradle build artifacts
# 2. Compile only the sentry_flutter plugin's Java/Kotlin code via Gradle
# (this does NOT require Dart to compile)
# 3. Create a stub libs.jar so jnigen's Gradle classpath resolution succeeds
# (libs.jar normally contains compiled Dart code from flutter build apk,
# but jnigen only needs the Java classes, not the Dart code)
# 4. Run jnigen directly to regenerate binding.dart
# 5. Verify the result by building the full APK

echo "Step 1: Cleaning Gradle build artifacts..."
cd example/android
./gradlew clean
cd ../..

echo "Step 2: Compiling sentry_flutter plugin Java/Kotlin code..."
cd example/android
./gradlew :sentry_flutter:bundleLibCompileToJarRelease
cd ../..

echo "Step 3: Creating stub libs.jar..."
# jnigen's Gradle classpath includes libs.jar (normally produced by flutter build apk).
# It must be a valid jar but doesn't need to contain Dart compiled classes —
# jnigen only reads Java classes from the Sentry Android SDK jars and Maven caches.
stub_dir=$(mktemp -d)
mkdir -p example/build/app/intermediates/flutter/release
jar cf example/build/app/intermediates/flutter/release/libs.jar -C "$stub_dir" .
rm -rf "$stub_dir"

echo "Step 4: Running jnigen..."
$DART run jnigen --config ffi-jni.yaml

echo "Step 5: Formatting generated binding..."
$DART format "$binding_path"

echo "Step 6: Verifying by building example APK..."
set +e
(
cd example
$FLUTTER build apk
)
verify_result=$?
set -e

if [ $verify_result -ne 0 ]; then
echo ""
echo "=== ERROR: Regenerated bindings do not compile ==="
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe work logging the error code or failure logs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaict the errors should be visible in the logs when running flutter build apk so we dont need to log more here

echo "=== Manual intervention is needed ==="
exit 1
fi

echo ""
echo "=== Fallback regeneration succeeded ==="
exit 0
Loading