Skip to content

Build and upload release version to Firebase App Distribution #455

Build and upload release version to Firebase App Distribution

Build and upload release version to Firebase App Distribution #455

Workflow file for this run

# 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