Files
Dariusz Koryto 4dfa1546d7 ci(mobile): retry bun install in mobile build workflow (#564)
Mirror the desktop-build retry loop so a transient tarball download
failure no longer fails the whole Android build.
2026-10-15 00:12:28 +08:00

363 lines
15 KiB
YAML

name: Build EdgeEver Mobile
on:
workflow_dispatch:
inputs:
release_tag:
description: "Upload the signed arm64 APK to this existing Draft GitHub Release"
required: true
type: string
release:
types: [published]
push:
branches:
- main
paths:
- "apps/mobile/**"
- "packages/client/**"
- "packages/shared/**"
- "bun.lock"
- ".github/workflows/mobile-build.yml"
- "scripts/build-android-local.sh"
- "scripts/verify-android-apk-signature.mjs"
- "scripts/download-play-universal-apk.mjs"
permissions:
actions: read
contents: write
concurrency:
group: edgeever-mobile-android-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
release-plan:
if: >-
${{
github.repository == 'tianma-if/edgeever' &&
(
github.event_name == 'release' ||
github.event_name == 'workflow_dispatch'
)
}}
name: Plan Android release asset
runs-on: ubuntu-latest
outputs:
asset_ready: ${{ steps.plan.outputs.asset_ready }}
previous_tag: ${{ steps.plan.outputs.previous_tag }}
rebuild: ${{ steps.plan.outputs.rebuild }}
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Validate Draft Release target
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
expected_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
test "$RELEASE_TAG" = "v${expected_version}"
test "$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')" = "true"
target_commitish="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json targetCommitish --jq '.targetCommitish')"
test "$(git rev-parse "${target_commitish}^{commit}")" = "$(git rev-parse "${GITHUB_SHA}^{commit}")"
- name: Set up Bun for release validation
uses: oven-sh/setup-bun@v2
- name: Install release validation dependencies
run: |
for attempt in 1 2 3; do
if bun install --frozen-lockfile; then
exit 0
fi
if [[ "$attempt" = "3" ]]; then
exit 1
fi
sleep "$((15 * attempt))"
done
- name: Run full project regression tests
run: bun run test
- name: Compare with previous formal release
id: plan
env:
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
GH_TOKEN: ${{ github.token }}
HEAD_REF: ${{ github.event_name == 'release' && github.event.release.tag_name || github.sha }}
run: |
mapfile -t release_tags < <(
gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
--jq '.[] | select(.draft == false and .prerelease == false) | .tag_name'
)
previous_tag=""
for release_tag in "${release_tags[@]}"; do
if [[ "$release_tag" != "$CURRENT_TAG" ]]; then
previous_tag="$release_tag"
break
fi
done
test -n "$previous_tag"
git fetch origin "refs/tags/${previous_tag}:refs/tags/${previous_tag}"
echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT"
plan_output="$(node scripts/plan-native-release.mjs mobile "$previous_tag" "$HEAD_REF")"
printf '%s\n' "$plan_output" >> "$GITHUB_OUTPUT"
rebuild="$(printf '%s\n' "$plan_output" | sed -n 's/^rebuild=//p')"
if [[ "$rebuild" = "true" ]]; then
root_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
mobile_version="$(node -p "JSON.parse(require('fs').readFileSync('apps/mobile/app.json', 'utf8')).expo.version")"
if [[ "$mobile_version" != "$root_version" ]]; then
echo "::error::Mobile rebuild requires apps/mobile/app.json expo.version ($mobile_version) to match package.json version ($root_version)."
exit 1
fi
current_version_code="$(node -p "JSON.parse(require('fs').readFileSync('apps/mobile/app.json', 'utf8')).expo.android.versionCode")"
previous_version_code="$(
git show "${previous_tag}:apps/mobile/app.json" |
node -e "const fs = require('fs'); console.log(JSON.parse(fs.readFileSync(0, 'utf8')).expo.android.versionCode)"
)"
if (( current_version_code <= previous_version_code )); then
echo "::error::Mobile rebuild requires android.versionCode ($current_version_code) to be greater than the previous release ($previous_version_code)."
exit 1
fi
fi
# `gh release view --json assets` stays empty after a draft is published.
# The API URL is addressed by release id and still lists the uploads.
release_api="$(gh release view "$CURRENT_TAG" --repo "$GITHUB_REPOSITORY" --json apiUrl --jq .apiUrl)"
mapfile -t current_assets < <(
gh api "$release_api" --jq '.assets[].name'
)
asset_ready="$(
printf '%s\n' "${current_assets[@]}" |
node scripts/check-native-release-assets.mjs mobile "$rebuild" "$CURRENT_TAG"
)"
echo "asset_ready=$asset_ready" >> "$GITHUB_OUTPUT"
if [[ "$GITHUB_EVENT_NAME" = "release" && "$asset_ready" != "true" ]]; then
echo "::error::Published Release assets are incomplete or incompatible. Restore the Release to Draft and prepare assets with workflow_dispatch."
exit 1
fi
- name: Verify existing APK uses the pinned signer
if: steps.plan.outputs.asset_ready == 'true'
env:
ANDROID_PLAY_APP_SIGNER_SHA256: ${{ secrets.ANDROID_PLAY_APP_SIGNER_SHA256 }}
EDGE_EVER_ANDROID_ALLOWED_SIGNER_SHA256: ${{ github.event_name == 'release' && secrets.ANDROID_PLAY_APP_SIGNER_SHA256 || format('22bf52a9501c89020f5acc966960152c826bfa64f31e578e858d088f8cd75d87,{0}', secrets.ANDROID_PLAY_APP_SIGNER_SHA256) }}
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p "$RUNNER_TEMP/native-assets"
node scripts/download-release-assets.mjs \
--repo "$GITHUB_REPOSITORY" \
--tag "$CURRENT_TAG" \
--dir "$RUNNER_TEMP/native-assets" \
--pattern 'edgeever-android-v*-arm64-v8a.apk'
apk_path="$(find "$RUNNER_TEMP/native-assets" -type f -name 'edgeever-android-v*-arm64-v8a.apk' -print -quit)"
test -n "$apk_path"
test -n "$ANDROID_PLAY_APP_SIGNER_SHA256"
node scripts/verify-android-apk-signature.mjs "$apk_path"
restore-invalid-published-release:
needs: release-plan
if: >-
${{
always() &&
github.repository == 'tianma-if/edgeever' &&
github.event_name == 'release' &&
needs.release-plan.result == 'failure'
}}
name: Restore invalid Android Release to Draft
runs-on: ubuntu-latest
steps:
- name: Restore Draft state
env:
CURRENT_TAG: ${{ github.event.release.tag_name }}
GH_TOKEN: ${{ github.token }}
run: gh release edit "$CURRENT_TAG" --repo "$GITHUB_REPOSITORY" --draft=true
reuse-release-apk:
needs: release-plan
if: >-
${{
github.repository == 'tianma-if/edgeever' &&
github.event_name == 'workflow_dispatch' &&
needs.release-plan.result == 'success' &&
needs.release-plan.outputs.asset_ready == 'false' &&
needs.release-plan.outputs.rebuild == 'false'
}}
name: Reuse Android release APK
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Copy latest compatible APK without renaming
env:
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
ANDROID_PLAY_APP_SIGNER_SHA256: ${{ secrets.ANDROID_PLAY_APP_SIGNER_SHA256 }}
EDGE_EVER_ANDROID_ALLOWED_SIGNER_SHA256: ${{ secrets.ANDROID_PLAY_APP_SIGNER_SHA256 }}
GH_TOKEN: ${{ github.token }}
PREVIOUS_TAG: ${{ needs.release-plan.outputs.previous_tag }}
run: |
mkdir -p "$RUNNER_TEMP/native-assets"
gh release download "$PREVIOUS_TAG" \
--repo "$GITHUB_REPOSITORY" \
--pattern 'edgeever-android-v*-arm64-v8a.apk' \
--dir "$RUNNER_TEMP/native-assets"
test "$(find "$RUNNER_TEMP/native-assets" -type f -name 'edgeever-android-v*-arm64-v8a.apk' | wc -l | tr -d ' ')" = "1"
apk_path="$(find "$RUNNER_TEMP/native-assets" -type f -name 'edgeever-android-v*-arm64-v8a.apk' -print -quit)"
test -n "$ANDROID_PLAY_APP_SIGNER_SHA256"
node scripts/verify-android-apk-signature.mjs "$apk_path"
gh release upload "$CURRENT_TAG" "$RUNNER_TEMP/native-assets/"* \
--repo "$GITHUB_REPOSITORY" \
--clobber
android:
needs: release-plan
if: >-
${{
always() &&
github.repository == 'tianma-if/edgeever' &&
(
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(
github.event_name == 'workflow_dispatch' &&
needs.release-plan.result == 'success' &&
needs.release-plan.outputs.asset_ready == 'false' &&
needs.release-plan.outputs.rebuild == 'true'
)
)
}}
name: Build Android packages
runs-on: [self-hosted, macOS, ARM64, edgeever-mobile]
timeout-minutes: 45
env:
ANDROID_HOME: /opt/homebrew/share/android-commandlinetools
ANDROID_SDK_ROOT: /opt/homebrew/share/android-commandlinetools
JAVA_HOME: /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
steps:
- name: Checkout
uses: actions/checkout@v5
with:
clean: true
- name: Install dependencies
run: |
for attempt in 1 2 3; do
if bun install --frozen-lockfile; then
exit 0
fi
if [[ "$attempt" = "3" ]]; then
exit 1
fi
sleep "$((15 * attempt))"
done
- name: Check Expo dependencies
run: |
if ! bun --cwd apps/mobile expo install --check; then
echo "::warning::Expo reported compatible patch-version updates; continuing with the frozen lockfile for this build."
fi
- name: Typecheck
run: bun run typecheck:mobile
- name: Reset Metro cache for this checkout
run: bun --cwd apps/mobile expo export --platform android --clear --max-workers 1 --output-dir "$RUNNER_TEMP/edgeever-metro-warmup"
- name: Restore Android signing keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
test -n "$ANDROID_KEYSTORE_BASE64"
printf '%s' "$ANDROID_KEYSTORE_BASE64" | openssl base64 -d -A > "$RUNNER_TEMP/edgeever-upload.p12"
chmod 600 "$RUNNER_TEMP/edgeever-upload.p12"
- name: Build fast main-branch APK
if: github.event_name == 'push'
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEYSTORE_FILE: ${{ runner.temp }}/edgeever-upload.p12
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
EDGE_EVER_ANDROID_APK_ARCHS: arm64-v8a
run: bun run build:android:fast
- name: Build signed release APK for GitHub Release
if: github.event_name == 'workflow_dispatch'
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEYSTORE_FILE: ${{ runner.temp }}/edgeever-upload.p12
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
EDGE_EVER_ANDROID_APK_ARCHS: arm64-v8a
run: bun run build:android:apk
- name: Verify release APK signature
if: github.event_name == 'workflow_dispatch'
env:
EDGE_EVER_ANDROID_ALLOWED_SIGNER_SHA256: 22bf52a9501c89020f5acc966960152c826bfa64f31e578e858d088f8cd75d87,${{ secrets.ANDROID_PLAY_APP_SIGNER_SHA256 }}
run: node scripts/verify-android-apk-signature.mjs apps/mobile/android/app/build/outputs/apk/release/app-release.apk
- name: Upload APK
if: github.event_name == 'push'
uses: actions/upload-artifact@v7
with:
name: edgeever-android-main-apk
path: apps/mobile/android/app/build/outputs/apk/release/*.apk
if-no-files-found: error
compression-level: 0
- name: Upload release APK to GitHub Release
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
release_asset="$RUNNER_TEMP/edgeever-android-${RELEASE_TAG}-arm64-v8a.apk"
cp apps/mobile/android/app/build/outputs/apk/release/app-release.apk "$release_asset"
gh release upload "$RELEASE_TAG" "$release_asset" --repo "$GITHUB_REPOSITORY" --clobber
- name: Remove temporary upload keystore
if: always()
run: rm -f "$RUNNER_TEMP/edgeever-upload.p12"
native-build-timings:
needs: android
if: >-
${{
always() &&
github.repository == 'tianma-if/edgeever' &&
needs.android.result != 'skipped'
}}
name: Report Android build timings
runs-on: ubuntu-latest
steps:
- name: Checkout timing reporter
uses: actions/checkout@v5
- name: Collect native build timings
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?filter=latest&per_page=100" \
> "$RUNNER_TEMP/actions-jobs.json"
node scripts/report-native-build-timings.mjs \
--input "$RUNNER_TEMP/actions-jobs.json" \
--platform android \
--json "$RUNNER_TEMP/native-build-timings.json" \
--markdown "$RUNNER_TEMP/native-build-timings.md"
cat "$RUNNER_TEMP/native-build-timings.md" >> "$GITHUB_STEP_SUMMARY"
- name: Upload machine-readable timing report
uses: actions/upload-artifact@v7
with:
name: edgeever-android-build-timings-attempt-${{ github.run_attempt }}
path: ${{ runner.temp }}/native-build-timings.json
if-no-files-found: error
retention-days: 90