Build and upload release version to Firebase App Distribution #453
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Release-candidate variant of release.yml for the ADFA-2602 toolchain upgrade. | |
| # Lives ONLY on task/ADFA-2602-release-candidate; never merged. Reuses the | |
| # registered release.yml name so workflow_dispatch works: GitHub runs this | |
| # branch's copy when the branch is selected. | |
| # | |
| # Differences from the stock release.yml: | |
| # - no merge_stage_to_main job (stock copy merges stage -> main; never | |
| # dispatch the stock copy) | |
| # - builds the dispatched branch, not main | |
| # - assets come from the adfa-2602-rc draft release on this repo, not from | |
| # the live dev-assets server (which still hosts the old toolchain) | |
| # - documentation.db.br comes from the same draft release, not Google Drive | |
| # - R2 upload / Slack / Telegram are gated behind the `publish` input | |
| # (default off); Firebase App Distribution to testers stays on | |
| name: Build release candidate from branch (ADFA-2602) | |
| permissions: | |
| id-token: write | |
| contents: write | |
| actions: write | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| asset_tag: | |
| description: "Draft release tag holding the nine release .br assets" | |
| required: false | |
| default: "adfa-2602-rc" | |
| publish: | |
| description: "Also upload to Cloudflare R2 and announce on Slack/Telegram" | |
| type: boolean | |
| required: false | |
| default: false | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| IDE_SIGNING_ALIAS: ${{ secrets.IDE_SIGNING_ALIAS }} | |
| IDE_SIGNING_AUTH_PASS: ${{ secrets.IDE_SIGNING_AUTH_PASS }} | |
| IDE_SIGNING_AUTH_USER: ${{ secrets.IDE_SIGNING_AUTH_USER }} | |
| IDE_SIGNING_KEY_PASS: ${{ secrets.IDE_SIGNING_KEY_PASS }} | |
| IDE_SIGNING_STORE_PASS: ${{ secrets.IDE_SIGNING_STORE_PASS }} | |
| IDE_SIGNING_URL: ${{ secrets.IDE_SIGNING_URL }} | |
| IDE_SIGNING_KEY_BIN: ${{ secrets.IDE_SIGNING_KEY_BIN }} | |
| ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MVN_USERNAME }} | |
| ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MVN_PASSWORD }} | |
| ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MVN_SIGNING_KEY }} | |
| ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.MVN_SIGNING_KEY_ID }} | |
| ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MVN_SIGNING_KEY_PASSWORD }} | |
| FIREBASE_CONSOLE_URL: ${{ secrets.FIREBASE_CONSOLE_URL }} | |
| GLITCHTIP_DSN: ${{ secrets.GLITCHTIP_DSN }} | |
| jobs: | |
| stage_assets: | |
| name: Stage candidate release assets | |
| runs-on: self-hosted | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Install jq | |
| run: command -v jq || { sudo apt-get update && sudo apt-get install -y jq; } | |
| - name: Download candidate assets from draft release | |
| env: | |
| ASSET_TAG: ${{ inputs.asset_tag }} | |
| run: | | |
| rm -rf .rc-assets && mkdir -p .rc-assets | |
| # gh release download can't be assumed on the runner; the REST API | |
| # lists drafts for tokens with push access, which GITHUB_TOKEN has. | |
| release_json=$(curl -sf -H "Authorization: Bearer $GH_TOKEN" \ | |
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=100") | |
| echo "$release_json" | jq -r --arg tag "$ASSET_TAG" \ | |
| '.[] | select(.tag_name == $tag) | .assets[] | "\(.id)\t\(.name)"' > .rc-assets/manifest.tsv | |
| [ -s .rc-assets/manifest.tsv ] || { echo "ERROR: no release with tag $ASSET_TAG or it has no assets"; exit 1; } | |
| while IFS=$'\t' read -r id name; do | |
| echo "downloading $name (asset $id)" | |
| curl -sfL -H "Authorization: Bearer $GH_TOKEN" \ | |
| -H "Accept: application/octet-stream" \ | |
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${id}" \ | |
| -o ".rc-assets/$name" | |
| done < .rc-assets/manifest.tsv | |
| rm .rc-assets/manifest.tsv | |
| ls -la .rc-assets | |
| - name: Lay out assets/release tree | |
| run: | | |
| REL=assets/release | |
| mkdir -p "$REL/common/data/common" "$REL/common/database" \ | |
| "$REL/v7/data/common" "$REL/v8/data/common" | |
| place() { # <file> <dest-dir> <min-bytes> | |
| src=".rc-assets/$1" | |
| [ -f "$src" ] || { echo "ERROR: missing asset $1"; exit 1; } | |
| size=$(stat -c%s "$src" 2>/dev/null || stat -f%z "$src") | |
| [ "$size" -ge "$3" ] || { echo "ERROR: $1 is only $size bytes"; exit 1; } | |
| mv -f "$src" "$2/" | |
| echo "staged $1 ($size bytes) -> $2" | |
| } | |
| place gradle-9.6.1-bin.zip.br "$REL/common/data/common" 50000000 | |
| place gradle-api-9.6.1.jar.br "$REL/common/data/common" 10000000 | |
| place localMvnRepository.zip.br "$REL/common/data/common" 50000000 | |
| place core.cgt.br "$REL/common/data/common" 100000 | |
| place documentation.db.br "$REL/common/database" 100000000 | |
| place android-sdk-v7.zip.br "$REL/v7/data/common" 30000000 | |
| place bootstrap-v7.zip.br "$REL/v7/data/common" 50000000 | |
| place android-sdk-v8.zip.br "$REL/v8/data/common" 30000000 | |
| place bootstrap-v8.zip.br "$REL/v8/data/common" 50000000 | |
| # the ABI files carry their target names inside the tree | |
| mv "$REL/v7/data/common/android-sdk-v7.zip.br" "$REL/v7/data/common/android-sdk.zip.br" | |
| mv "$REL/v7/data/common/bootstrap-v7.zip.br" "$REL/v7/data/common/bootstrap.zip.br" | |
| mv "$REL/v8/data/common/android-sdk-v8.zip.br" "$REL/v8/data/common/android-sdk.zip.br" | |
| mv "$REL/v8/data/common/bootstrap-v8.zip.br" "$REL/v8/data/common/bootstrap.zip.br" | |
| - name: Upload release assets | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: assets | |
| path: assets/release | |
| retention-days: 1 | |
| build_apk: | |
| name: Build Release Candidate APK | |
| runs-on: self-hosted | |
| timeout-minutes: 90 | |
| needs: stage_assets | |
| strategy: | |
| matrix: | |
| include: | |
| - variant: v8 | |
| build_type: "RELEASE CANDIDATE 64-bit (ADFA-2602)" | |
| - variant: v7 | |
| build_type: "RELEASE CANDIDATE 32-bit (ADFA-2602)" | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download release assets | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: assets | |
| path: assets/release | |
| - name: Initialize submodules | |
| run: | | |
| git submodule init | |
| git submodule update --remote | |
| - name: Check if Nix is installed | |
| id: check_nix | |
| run: | | |
| if command -v nix >/dev/null 2>&1; then | |
| echo "nix_installed=true" >> $GITHUB_ENV | |
| else | |
| echo "nix_installed=false" >> $GITHUB_ENV | |
| fi | |
| - name: Install Flox | |
| if: env.nix_installed == 'false' | |
| uses: flox/install-flox-action@v2 | |
| - name: Create google-services.json | |
| env: | |
| GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} | |
| run: | | |
| echo "$GOOGLE_SERVICES_JSON" > app/google-services.json | |
| - name: Validate release version | |
| env: | |
| NEXT_RELEASE_VERSION: ${{ vars.NEXT_RELEASE_VERSION }} | |
| run: | | |
| if [ -z "$NEXT_RELEASE_VERSION" ]; then | |
| echo "ERROR: NEXT_RELEASE_VERSION variable is not set" | |
| exit 1 | |
| fi | |
| if [[ ! "$NEXT_RELEASE_VERSION" =~ ^[0-9]{2}\.[0-9]{2}$ ]]; then | |
| echo "ERROR: NEXT_RELEASE_VERSION format is invalid: $NEXT_RELEASE_VERSION" | |
| exit 1 | |
| fi | |
| - name: Install advancecomp (advzip, used by recompressApk) | |
| run: | | |
| command -v advzip || { sudo apt-get update && sudo apt-get install -y advancecomp; } | |
| - name: Assemble Release APK | |
| env: | |
| INCLUDE_LLAMA_ASSETS: "true" | |
| NEXT_RELEASE_VERSION: ${{ vars.NEXT_RELEASE_VERSION }} | |
| run: | | |
| variant_upper=$(echo "${{ matrix.variant }}" | tr '[:lower:]' '[:upper:]') | |
| flox activate -d flox/base -- ./gradlew :app:assemble${variant_upper}Release --no-daemon \ | |
| -Pnext_release_version="$NEXT_RELEASE_VERSION" \ | |
| -Dorg.gradle.jvmargs="-Xmx10g -XX:MaxMetaspaceSize=2g -XX:+HeapDumpOnOutOfMemoryError --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED" \ | |
| -Dandroid.aapt2.daemonHeapSize=4096M \ | |
| -Dorg.gradle.workers.max=1 \ | |
| -Dorg.gradle.parallel=false | |
| - name: Find APK file | |
| id: find_apk | |
| run: | | |
| apk_path=$(find app/build/outputs/apk/ -path "*${{ matrix.variant }}*/release/*.apk" | head -n 1) | |
| echo "APK_PATH=$apk_path" >> $GITHUB_OUTPUT | |
| - name: Get Commit Information | |
| id: commit_info | |
| run: | | |
| COMMIT_MSG=$(git log -1 --pretty=%B | head -1 | tr -d '\n\r' | sed 's/[*]/-/g') | |
| echo "COMMIT_MSG=$COMMIT_MSG" >> $GITHUB_OUTPUT | |
| - name: Authenticate to Google Cloud via Workload Identity | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| workload_identity_provider: ${{ secrets.WIF_PROVIDER }} | |
| service_account: ${{ secrets.IDENTITY_EMAIL }} | |
| - name: Verify APK exists | |
| run: | | |
| if [ ! -f "${{ steps.find_apk.outputs.APK_PATH }}" ]; then | |
| echo "ERROR: APK file not found" | |
| exit 1 | |
| fi | |
| ls -la "${{ steps.find_apk.outputs.APK_PATH }}" | |
| - name: Upload APK as run artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: candidate-apk-${{ matrix.variant }} | |
| path: ${{ steps.find_apk.outputs.APK_PATH }} | |
| retention-days: 7 | |
| if-no-files-found: error | |
| - name: Deploy to Firebase App Distribution | |
| env: | |
| APK_PATH: ${{ steps.find_apk.outputs.APK_PATH }} | |
| FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} | |
| BUILD_TYPE: ${{ matrix.build_type }} | |
| COMMIT_MSG: ${{ steps.commit_info.outputs.COMMIT_MSG }} | |
| run: | | |
| firebase projects:list || { | |
| echo "ERROR: Firebase authentication failed" | |
| exit 1 | |
| } | |
| RELEASE_NOTES_FILE=$(mktemp) | |
| cat > "$RELEASE_NOTES_FILE" << EOF | |
| Build Type: $BUILD_TYPE | |
| Branch: ${{ github.ref_name }} | |
| Commit: $COMMIT_MSG | |
| EOF | |
| firebase appdistribution:distribute "$APK_PATH" \ | |
| --app "$FIREBASE_APP_ID" \ | |
| --groups "testers" \ | |
| --release-notes-file "$RELEASE_NOTES_FILE" | |
| - name: Install uv | |
| if: ${{ inputs.publish }} | |
| uses: astral-sh/setup-uv@v6 | |
| - name: Upload APK to Cloudflare R2 | |
| if: ${{ inputs.publish }} | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_KEY_ID: ${{ vars.CLOUDFLARE_KEY_ID }} | |
| CLOUDFLARE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }} | |
| run: | | |
| uv run --with boto3 scripts/cloudflare-r2-upload.py "${{ steps.find_apk.outputs.APK_PATH }}" | |
| - name: Send Slack Notification | |
| if: ${{ inputs.publish }} | |
| env: | |
| SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| COMMIT_MSG: ${{ steps.commit_info.outputs.COMMIT_MSG }} | |
| run: | | |
| jq -n \ | |
| --arg commit_msg "$COMMIT_MSG" \ | |
| --arg build_type "${{ matrix.build_type }}" \ | |
| --arg branch_name "${{ github.ref_name }}" \ | |
| '{ | |
| blocks: [ | |
| { type: "header", text: { type: "plain_text", text: "Release candidate build available", emoji: true } }, | |
| { type: "section", text: { type: "mrkdwn", text: "*Build Type:* \($build_type)\n*Commit:* \($commit_msg)\n*Branch:* `\($branch_name)`" } } | |
| ] | |
| }' > payload.json | |
| curl -X POST -H "Content-type: application/json" --data @payload.json "$SLACK_WEBHOOK" | |
| rm -f payload.json | |
| - name: Clean up build folder after upload | |
| run: | | |
| rm -rf app/build/ | |
| - name: Cleanup google-services.json | |
| if: always() | |
| run: | | |
| rm -f app/google-services.json |