Skip to content

Release Android

Release Android #12

Workflow file for this run

name: Release Android
on:
workflow_dispatch:
inputs:
version:
description: 'Version tag using CalVer (e.g., v2025.12.1 for Dec 2025, patch 1)'
required: true
type: string
upload_to_play:
description: 'Upload to Google Play Internal track'
required: false
default: false
type: boolean
permissions:
contents: write
jobs:
# Get version and git info once for all jobs, and run tests if needed
version-info:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
version_number: ${{ steps.version.outputs.version_number }}
git_tag: ${{ steps.git_version.outputs.git_tag }}
git_commit: ${{ steps.git_version.outputs.git_commit }}
git_branch: ${{ steps.git_version.outputs.git_branch }}
build_version: ${{ steps.git_version.outputs.version }}
build_time: ${{ steps.git_version.outputs.build_time }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get version from tag
id: version
run: |
VERSION="${{ inputs.version }}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
- name: Get git version info
id: git_version
run: scripts/get_version_info.sh github >> $GITHUB_OUTPUT
- name: Setup Flutter App
uses: ./.github/actions/setup-flutter-app
with:
google-services-json: ${{ secrets.GOOGLE_SERVICES_JSON }}
generate-mocks: 'true'
- name: Run tests
run: flutter test
# Build APK and AAB in parallel using matrix strategy
build-artifacts:
needs: version-info
runs-on: ubuntu-latest
strategy:
matrix:
build-type:
- name: apk
flutter-command: apk
output-path: build/app/outputs/flutter-apk/app-release.apk
artifact-name: android-apk
- name: appbundle
flutter-command: appbundle
output-path: build/app/outputs/bundle/release/app-release.aab
artifact-name: android-aab
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup Flutter App
uses: ./.github/actions/setup-flutter-app
with:
google-services-json: ${{ secrets.GOOGLE_SERVICES_JSON }}
generate-mocks: 'true'
- name: Decode upload keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
missing=0
[ -z "$ANDROID_KEYSTORE_BASE64" ] && echo "::error::ANDROID_KEYSTORE_BASE64 secret is not set. See docs/tooling/github-secrets.md" && missing=1
[ -z "$ANDROID_KEYSTORE_PASSWORD" ] && echo "::error::ANDROID_KEYSTORE_PASSWORD secret is not set. See docs/tooling/github-secrets.md" && missing=1
[ -z "$ANDROID_KEY_PASSWORD" ] && echo "::error::ANDROID_KEY_PASSWORD secret is not set. See docs/tooling/github-secrets.md" && missing=1
[ -z "$ANDROID_KEY_ALIAS" ] && echo "::error::ANDROID_KEY_ALIAS secret is not set. See docs/tooling/github-secrets.md" && missing=1
[ "$missing" -eq 1 ] && exit 1
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/upload-keystore.jks
- name: Create key.properties
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
printf 'storePassword=%s\nkeyPassword=%s\nkeyAlias=%s\nstoreFile=%s\n' \
"$ANDROID_KEYSTORE_PASSWORD" \
"$ANDROID_KEY_PASSWORD" \
"$ANDROID_KEY_ALIAS" \
"../upload-keystore.jks" \
> android/key.properties
- name: Build release ${{ matrix.build-type.name }}
run: |
flutter build ${{ matrix.build-type.flutter-command }} --release \
--dart-define=GIT_TAG=${{ needs.version-info.outputs.git_tag }} \
--dart-define=GIT_COMMIT=${{ needs.version-info.outputs.git_commit }} \
--dart-define=GIT_BRANCH=${{ needs.version-info.outputs.git_branch }} \
--dart-define=BUILD_VERSION=${{ needs.version-info.outputs.build_version }} \
--dart-define=BUILD_TIME=${{ needs.version-info.outputs.build_time }}
- name: Upload ${{ matrix.build-type.name }} artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.build-type.artifact-name }}
path: ${{ matrix.build-type.output-path }}
if-no-files-found: error
retention-days: 7
- name: Upload mapping file (deobfuscation)
if: matrix.build-type.name == 'appbundle'
uses: actions/upload-artifact@v4
with:
name: android-mapping
path: build/app/outputs/mapping/release/mapping.txt
if-no-files-found: error
retention-days: 7
# Create release with all artifacts
create-release:
needs: [version-info, build-artifacts]
runs-on: ubuntu-latest
steps:
- name: Download APK artifact
uses: actions/download-artifact@v4
with:
name: android-apk
path: artifacts/apk
- name: Download AAB artifact
uses: actions/download-artifact@v4
with:
name: android-aab
path: artifacts/aab
- name: Rename artifacts with version
run: |
VERSION="${{ needs.version-info.outputs.version_number }}"
mkdir -p release-artifacts
cp artifacts/apk/app-release.apk "release-artifacts/cambridge-beer-festival-${VERSION}.apk"
cp artifacts/aab/app-release.aab "release-artifacts/cambridge-beer-festival-${VERSION}.aab"
- name: Generate checksums
working-directory: release-artifacts
run: |
sha256sum *.apk > checksums.txt
sha256sum *.aab >> checksums.txt
cat checksums.txt
- name: Upload Android artifacts to Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version-info.outputs.version }}
append_body: true
body: |
---
### Android
- **APK** (`cambridge-beer-festival-${{ needs.version-info.outputs.version_number }}.apk`) — direct install (requires "Install from unknown sources")
- **AAB** (`cambridge-beer-festival-${{ needs.version-info.outputs.version_number }}.aab`) — Google Play upload; Play re-signs with the app signing key
Package: `ralcock.cbf` | Min SDK: API 21 | Target SDK: API 34
files: |
release-artifacts/*.apk
release-artifacts/*.aab
release-artifacts/checksums.txt
publish-google-play:
needs: [version-info, build-artifacts]
# Only upload when explicitly requested — automated releases pass upload_to_play=true
if: inputs.upload_to_play
# Allow the workflow to succeed even if Play upload fails (e.g. before first manual upload)
continue-on-error: true
runs-on: ubuntu-latest
steps:
- name: Download AAB artifact
uses: actions/download-artifact@v4
with:
name: android-aab
path: artifacts/aab
- name: Download mapping file artifact
uses: actions/download-artifact@v4
with:
name: android-mapping
path: artifacts/mapping
- name: Upload to Google Play (Internal track)
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: ralcock.cbf
releaseFiles: artifacts/aab/app-release.aab
mappingFile: artifacts/mapping/mapping.txt
track: internal
status: completed