gh release download can see no assets after a draft is published, so the Linux and Android publication audits fail and the Release returns to Draft. Download those audit files through the release API URL.
345 lines
14 KiB
YAML
345 lines
14 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: bun install --frozen-lockfile
|
|
|
|
- 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: bun install --frozen-lockfile
|
|
|
|
- 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
|