name: Mobile Android Release # Why a separate workflow from iOS: iOS releases go through App Store review, # which can take days. Decoupling the triggers lets an Android release ship # immediately without waiting on iOS, and vice versa. on: push: tags: - 'mobile-android-v*' workflow_dispatch: inputs: release_version: description: 'Optional exact mobile marketing version assertion, e.g. 0.0.22' required: false type: string publish_github_release: description: 'Create or update the mobile-android-v GitHub Release' required: false default: true type: boolean jobs: android-build: runs-on: ubuntu-latest # Why: the "Create GitHub Release" step below uses the default GITHUB_TOKEN # to call `gh release create`, which requires contents:write. Without this, # the job builds the APK fine but fails at release time with # "HTTP 403: Resource not accessible by integration". permissions: contents: write defaults: run: working-directory: mobile steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Setup pnpm uses: pnpm/setup@v2 with: install: false - name: Install dependencies run: pnpm install --frozen-lockfile - name: Resolve release version and version code id: release env: MOBILE_ANDROID_RELEASE_VERSION: ${{ github.event.inputs.release_version }} MOBILE_ANDROID_PUBLISH_RELEASE: ${{ github.event.inputs.publish_github_release }} run: node scripts/prepare-android-release.mjs - name: Setup JDK 17 uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 - name: Expo prebuild run: npx expo prebuild --platform android --no-install - name: Build Android release APK run: cd android && ./gradlew assembleRelease - name: Upload APK artifact uses: actions/upload-artifact@v7 with: name: orca-mobile-apk path: mobile/android/app/build/outputs/apk/release/*.apk - name: Ensure GitHub release tag if: steps.release.outputs.publish_release == 'true' && !startsWith(github.ref, 'refs/tags/mobile-android-v') run: | set -euo pipefail tag="${{ steps.release.outputs.tag }}" if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then echo "Tag $tag already exists" exit 0 fi git tag "$tag" "$GITHUB_SHA" git push origin "refs/tags/$tag" - name: Create GitHub Release if: steps.release.outputs.publish_release == 'true' env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail tag="${{ steps.release.outputs.tag }}" if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then gh release upload "$tag" \ --repo "$GITHUB_REPOSITORY" \ --clobber \ android/app/build/outputs/apk/release/*.apk else # Why: release tags live on side branches, so GitHub's automatic # previous-tag detection reaches back several releases; that body # already exceeds the 125000-character API limit and grows each # release. Pin the comparison base and cap the size. notes_file="$RUNNER_TEMP/android-release-notes.md" previous_tag="$( gh release list --repo "$GITHUB_REPOSITORY" --limit 200 --json tagName --jq '.[].tagName' \ | grep '^mobile-android-v' | grep -Fxv "$tag" | sort -V | tail -1 || true )" if [ -n "$previous_tag" ]; then # Why: gh writes the JSON error body to stdout on an HTTP error, so a # non-empty file is not proof of success — gate on exit status. if ! gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" -X POST \ -f tag_name="$tag" \ -f target_commitish="$GITHUB_SHA" \ -f previous_tag_name="$previous_tag" \ --jq .body > "$notes_file"; then : > "$notes_file" fi fi if [ ! -s "$notes_file" ]; then printf 'Orca Mobile Android %s\n' "$tag" > "$notes_file" fi # Why: reuse the desktop release path's character-safe truncation so a # multi-byte character cannot be split at the cap. NOTES_FILE="$notes_file" \ NOTES_MODULE="$GITHUB_WORKSPACE/config/scripts/create-draft-release.mjs" \ node --input-type=module -e ' const { readFileSync, writeFileSync } = await import("node:fs") const { pathToFileURL } = await import("node:url") const { truncateReleaseBody } = await import(pathToFileURL(process.env.NOTES_MODULE).href) const file = process.env.NOTES_FILE writeFileSync(file, truncateReleaseBody(readFileSync(file, "utf8"))) ' gh release create "$tag" \ --repo "$GITHUB_REPOSITORY" \ --title "Orca Mobile Android $tag" \ --prerelease \ --latest=false \ --notes-file "$notes_file" \ android/app/build/outputs/apk/release/*.apk fi