name: Android Store Artifacts on: workflow_dispatch: inputs: version_name: description: Store version name, for example 1.4.0 required: false type: string version_code: description: Store build number/version code required: false type: string upload_android_to_play: description: Upload the signed Android App Bundle to Google Play required: false type: boolean default: true android_track: description: Google Play track for manual dispatches required: false type: choice default: production options: - production - beta - alpha - internal android_release_status: description: Google Play release status for manual dispatches required: false type: choice default: inProgress options: - inProgress - draft - halted push: tags: - "mobile-v*" permissions: contents: read actions: read concurrency: group: android-store-artifacts-${{ github.ref_name || github.run_id }} cancel-in-progress: true jobs: android: name: Android AAB and Play upload runs-on: ubuntu-24.04 environment: mobile-store-production timeout-minutes: 60 env: ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME || 'io.truckwash.twa' }} ANDROID_AAB_PATH: ${{ vars.ANDROID_AAB_PATH || 'android/app/build/outputs/bundle/release/app-release.aab' }} ANDROID_SIGNING_IDENTITY_REF: github-environment:mobile-store-production/android-keystore PLAY_STORE_TRACK: ${{ inputs.android_track || vars.PLAY_STORE_TRACK || 'production' }} PLAY_STORE_RELEASE_STATUS: ${{ inputs.android_release_status || 'inProgress' }} PLAY_STORE_USER_FRACTION: "0.01" UPLOAD_ANDROID_TO_PLAY: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_android_to_play }} steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.sha }} persist-credentials: false - name: Guard current master release id: release-guard shell: bash env: EVENT_NAME: ${{ github.event_name }} EXPECTED_SHA: ${{ github.sha }} RELEASE_BRANCH: ${{ github.ref_name }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} UPLOAD_TO_PLAY: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_android_to_play }} GH_TOKEN: ${{ github.token }} run: | set -euo pipefail current=true latest_sha="$(curl --fail --silent --show-error --location \ -H "Authorization: Bearer $GH_TOKEN" \ -H "Accept: application/vnd.github+json" \ "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/$DEFAULT_BRANCH" | jq -r '.object.sha // empty')" if [[ ! "$latest_sha" =~ ^[0-9a-f]{40}$ ]]; then echo "Could not resolve origin/$DEFAULT_BRANCH." >&2 exit 1 fi if [[ "$latest_sha" != "$EXPECTED_SHA" && "$UPLOAD_TO_PLAY" == "true" ]]; then current=false echo "Skipping stale mobile upload for $EXPECTED_SHA; origin/$DEFAULT_BRANCH is $latest_sha." elif [[ "$latest_sha" != "$EXPECTED_SHA" ]]; then echo "Allowing artifact-only build for $EVENT_NAME on $RELEASE_BRANCH; store upload remains disabled." else echo "Mobile upload commit is current for $DEFAULT_BRANCH." fi echo "current=$current" >> "$GITHUB_OUTPUT" echo "source_sha=$latest_sha" >> "$GITHUB_OUTPUT" - name: Setup Node.js if: steps.release-guard.outputs.current == 'true' uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 cache: npm - name: Require green Chromium mobile tests before Play upload if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true' run: node scripts/mobile/verify-store-test-gate.mjs --platform android env: GH_TOKEN: ${{ github.token }} STORE_SOURCE_SHA: ${{ steps.release-guard.outputs.source_sha }} TEST_WORKFLOW_RUN_ID: "" DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - name: Setup Java if: steps.release-guard.outputs.current == 'true' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin java-version: 21 - name: Setup Android SDK if: steps.release-guard.outputs.current == 'true' uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 - name: Install Android SDK packages if: steps.release-guard.outputs.current == 'true' shell: bash run: | set -euo pipefail yes | sdkmanager --licenses >/dev/null || true sdkmanager "platforms;android-36" "build-tools;36.0.0" - name: Resolve mobile version if: steps.release-guard.outputs.current == 'true' shell: bash env: INPUT_VERSION_NAME: ${{ inputs.version_name || '' }} INPUT_VERSION_CODE: ${{ inputs.version_code || '' }} run: | set -euo pipefail version_name="$INPUT_VERSION_NAME" if [[ -z "$version_name" && "$GITHUB_REF_NAME" == mobile-v* ]]; then version_name="${GITHUB_REF_NAME#mobile-v}" fi if [[ -z "$version_name" ]]; then version_name="0.0.${GITHUB_RUN_NUMBER}" fi version_code="${INPUT_VERSION_CODE:-$GITHUB_RUN_NUMBER}" echo "MOBILE_VERSION_NAME=$version_name" >> "$GITHUB_ENV" echo "MOBILE_VERSION_CODE=$version_code" >> "$GITHUB_ENV" - name: Check Android store environment if: steps.release-guard.outputs.current == 'true' env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }} run: node scripts/mobile/check-store-upload-env.mjs --android - name: Install dependencies if: steps.release-guard.outputs.current == 'true' run: npm ci --legacy-peer-deps - name: Decode Android signing key if: steps.release-guard.outputs.current == 'true' shell: bash env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | set -euo pipefail keystore_path="$RUNNER_TEMP/android-release.keystore" node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.ANDROID_KEYSTORE_BASE64, 'base64'))" "$keystore_path" echo "ANDROID_KEYSTORE_FILE=$keystore_path" >> "$GITHUB_ENV" echo "ANDROID_KEYSTORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD" >> "$GITHUB_ENV" echo "ANDROID_KEY_ALIAS=$ANDROID_KEY_ALIAS" >> "$GITHUB_ENV" echo "ANDROID_KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> "$GITHUB_ENV" - name: Build and sync Android shell if: steps.release-guard.outputs.current == 'true' run: | npm run mobile:android:sync npm run mobile:permissions:check npm run mobile:android:signing:check - name: Build signed Android App Bundle if: steps.release-guard.outputs.current == 'true' working-directory: android run: ./gradlew --no-daemon bundleRelease - name: Verify Android App Bundle signature if: steps.release-guard.outputs.current == 'true' run: jarsigner -verify -certs -verbose "$ANDROID_AAB_PATH" >/dev/null - name: Record immutable Android artifact proof if: steps.release-guard.outputs.current == 'true' id: artifact-proof shell: bash run: | set -euo pipefail artifact_sha256="$(sha256sum "$ANDROID_AAB_PATH" | awk '{print $1}')" [[ "$artifact_sha256" =~ ^[0-9a-f]{64}$ ]] echo "ANDROID_AAB_SHA256=$artifact_sha256" >> "$GITHUB_ENV" echo "sha256=$artifact_sha256" >> "$GITHUB_OUTPUT" echo "Android AAB SHA-256: \`$artifact_sha256\`" >> "$GITHUB_STEP_SUMMARY" - name: Upload Android artifact if: steps.release-guard.outputs.current == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: truck-wash-android-${{ env.MOBILE_VERSION_NAME }}-${{ github.sha }} path: ${{ env.ANDROID_AAB_PATH }} if-no-files-found: error retention-days: 14 - name: Recheck live master before Play upload if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true' env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} EXPECTED_SHA: ${{ steps.release-guard.outputs.source_sha }} GH_TOKEN: ${{ github.token }} run: | set -euo pipefail live_master_sha="$(curl --fail --silent --show-error --location \ -H "Authorization: Bearer $GH_TOKEN" \ -H "Accept: application/vnd.github+json" \ "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/$DEFAULT_BRANCH" | jq -r '.object.sha // empty')" [[ "$live_master_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "Could not resolve origin/$DEFAULT_BRANCH." >&2; exit 1; } [[ "$live_master_sha" == "$EXPECTED_SHA" ]] || { echo "$DEFAULT_BRANCH advanced while the Android bundle was building; refusing Play upload." >&2 exit 1 } - name: Upload Android App Bundle to Google Play if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true' id: play-upload env: GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }} run: npm run mobile:android:play-upload - name: Record Google Play submission proof if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true' env: ARTIFACT_SHA256: ${{ steps.artifact-proof.outputs.sha256 }} PLAY_EDIT_ID: ${{ steps.play-upload.outputs.play_edit_id }} PLAY_VERSION_CODE: ${{ steps.play-upload.outputs.version_code }} run: | set -euo pipefail test -n "$ARTIFACT_SHA256" test -n "$PLAY_EDIT_ID" test -n "$PLAY_VERSION_CODE" printf 'Google Play submission proof: platform=android applicationId=%s version=%s buildNumber=%s artifactSha256=%s signingIdentityRef=%s storeSubmissionId=%s status=%s fraction=%s\n' \ "$ANDROID_PACKAGE_NAME" "$MOBILE_VERSION_NAME" "$PLAY_VERSION_CODE" \ "$ARTIFACT_SHA256" "$ANDROID_SIGNING_IDENTITY_REF" "$PLAY_EDIT_ID" \ "$PLAY_STORE_RELEASE_STATUS" "$PLAY_STORE_USER_FRACTION" >> "$GITHUB_STEP_SUMMARY"