This guide covers how to create and publish Android releases for the Cambridge Beer Festival app.
This app uses Calendar Versioning (CalVer) with the format: YYYY.MM.PATCH
- YYYY: 4-digit year (e.g., 2025)
- MM: Month without leading zero (1-12, not 01-12)
- PATCH: Incremental patch number starting from 0
v2025.12.0- First release in December 2025v2025.12.1- Second release in December 2025 (patch/hotfix)v2026.1.0- First release in January 2026v2026.1.1- Second release in January 2026
The Android versionCode (integer, from the suffix after + in pubspec.yaml) is
generated as:
versionCode = (YYYYMMDD * 100) + PATCH
Examples:
2026.5.0on 2026-05-17 →20260517002026.5.1on 2026-05-17 →20260517012026.6.0on 2026-06-03 →2026060300
This ensures:
- Each version has a unique, incrementing code
- Google Play accepts it as an upgrade
- Maximum value: 2,147,483,647 (year 214748)
Before creating a release, update the version in pubspec.yaml:
version: 2026.5.7+2026051707Format: versionName+versionCode
- versionName: Human-readable CalVer (e.g.,
2026.5.7) - versionCode: Integer for Play Store ordering (e.g.,
2026051707)
git add pubspec.yaml
git commit -m "Bump version to v2025.12.0"
git push origin main# Create annotated tag
git tag -a v2025.12.0 -m "Release v2025.12.0"
# Push tag to trigger release workflow
git push origin v2025.12.0The release workflow will automatically:
- Run tests
- Build signed APK and AAB (using upload keystore)
- Generate SHA256 checksums
- Create a GitHub Release with:
- Release notes (auto-generated from commits)
- APK file for direct installation
- AAB file for Play Store upload
- Checksums file
- Upload the signed AAB to Google Play Internal track automatically
First release only: The Google Play API cannot create a new app listing. Before CI upload will work, you must upload the first AAB manually through Play Console and complete the app listing setup (store page, content rating, privacy policy). After that, all future releases are fully automated. See play-store.md for the first-time setup checklist.
You can also trigger a release manually via GitHub Actions:
- Go to Actions → Release workflow
- Click Run workflow
- Enter the version tag (e.g.,
v2025.12.0) - Click Run workflow
Before creating your first production release, test the workflow to ensure everything works correctly.
Best for: Testing the full workflow without creating a real release tag.
Steps:
-
Merge this PR to main (or push to your branch)
-
Navigate to GitHub Actions
- Go to repository → Actions tab
- Select Release workflow from the left sidebar
-
Trigger manually
- Click Run workflow (dropdown button)
- Branch: Select
main(or your current branch) - Version: Enter a test version like
v2025.12.0-test - Click Run workflow button
-
Monitor the workflow
- Watch the workflow run in real-time
- Check for any errors in build steps
- Verify all steps complete successfully
-
Verify outputs
- Check that a GitHub Release was created
- Download and verify APK and AAB files
- Check checksums file is present
- Test installing the APK on a device
-
Clean up
- Delete the test release from Releases page
- No need to delete tags (it wasn't created via tag push)
Advantages:
- ✅ Full workflow validation
- ✅ No tag management needed
- ✅ Easy to repeat
- ✅ Can test from any branch
- ✅ Free (GitHub Actions minutes included for public repos)
Time: ~5-10 minutes per test run
Best for: Testing the tag-triggered workflow.
Steps:
# Create test tag
git tag -a v2025.12.0-test -m "Test release workflow"
# Push tag to trigger workflow
git push origin v2025.12.0-test
# Monitor workflow in GitHub Actions tab
# After testing, delete the tag
git tag -d v2025.12.0-test # Delete locally
git push origin :refs/tags/v2025.12.0-test # Delete remotely
# Also delete the GitHub Release if created
# Go to Releases → Click release → Delete releaseAdvantages:
- ✅ Tests the actual tag-based trigger
- ✅ Validates the complete automated flow
Disadvantages:
⚠️ Creates a real tag (needs cleanup)⚠️ Tag appears in git history even after deletion
Best for: Quick validation of build configuration without using CI/CD minutes.
Requirements:
- Flutter installed locally
- Android SDK configured
- Java 17+ installed
Steps:
# Get dependencies
flutter pub get
# Build release APK
flutter build apk --release
# Build release AAB
flutter build appbundle --release
# Check outputs
ls -lh build/app/outputs/flutter-apk/app-release.apk
ls -lh build/app/outputs/bundle/release/app-release.aab
# Test install on device (optional)
adb install build/app/outputs/flutter-apk/app-release.apkAdvantages:
- ✅ Fastest feedback
- ✅ No GitHub Actions usage
- ✅ Can iterate quickly
Disadvantages:
- ❌ Doesn't test the full CI/CD workflow
- ❌ Doesn't test release creation
- ❌ Requires local Flutter setup
First time setup:
- Local build test - Verify builds work
- Manual trigger - Test full workflow with
v2025.12.0-test - Verify everything - Download artifacts, test APK
- Clean up test release - Delete from Releases page
- Create real release - Use tag
v2025.12.0for production
For subsequent releases:
Just create the production tag directly - you've already validated the workflow works.
When testing, check these items:
- Workflow completes successfully (all green checkmarks)
- GitHub Release is created with correct version number
- APK file is present and downloads successfully
- AAB file is present and downloads successfully
- Checksums file is present with both file hashes
- Release notes are generated (auto-generated from commits)
- APK installs on Android device (test with
adb install) - App launches and runs without crashes
- Version number shows correctly in app (check About screen if available)
Build fails: "Flutter not found"
- Workflow uses Flutter 3.44.0 - this is correct
- Check if Flutter version changed in workflow
Build fails: "Gradle error"
- Check
android/app/build.gradlesyntax - Verify signing config is correct
APK/AAB missing
- Check build step completed successfully
- Verify artifact upload paths are correct
Release not created
- Check workflow has
contents: writepermission - Verify
GITHUB_TOKENis available (automatic)
Release notes empty
- Make sure you have commits since last tag
- Release notes are auto-generated from git history
The release workflow includes several performance optimizations:
Gradle Dependency Caching:
- Caches
~/.gradle/cachesand~/.gradle/wrapperbetween builds - Reduces build time by 2-5 minutes on cache hits
- First build after cache clear takes normal duration
Gradle Build Cache:
- Enabled via
org.gradle.caching=trueinandroid/gradle.properties - Enables incremental builds (1-3 min savings)
- Reuses build outputs from previous builds when inputs haven't changed
Parallel Execution:
- Enabled via
org.gradle.parallel=trueinandroid/gradle.properties - Runs independent Gradle tasks in parallel
- Better utilizes available CPU cores
Total estimated savings: 5-12 minutes per build (after initial cache population)
When building locally, the same optimizations apply:
# First build - slower (populates cache)
flutter build apk --release
# Subsequent builds - faster (uses cache)
flutter build apk --releaseTip: Keep your local Gradle cache to speed up builds:
- Cache location:
~/.gradle/caches - Typical size: 1-3 GB
- Only clear if troubleshooting build issues
The android/gradle.properties file includes:
org.gradle.caching=true # Build cache
org.gradle.parallel=true # Parallel executionNote: org.gradle.configureondemand is intentionally NOT used as it's deprecated in Gradle 8.9.1+ and can cause issues with Flutter's multi-project builds.
Each release produces three files:
- Filename:
cambridge-beer-festival-YYYY.MM.PATCH.apk - Size: ~15-25 MB
- Use: Direct installation on Android devices
- Installation: Requires "Install from unknown sources" enabled
- Signing: Signed with upload keystore in CI
- Filename:
cambridge-beer-festival-YYYY.MM.PATCH.aab - Size: ~10-15 MB (smaller than APK)
- Use: Uploaded automatically to Google Play Internal track by CI
- Signing: Signed with upload key; Google Play re-signs with the app signing key (Play App Signing)
- Filename:
checksums.txt - Content: SHA256 hashes of APK and AAB
- Use: Verify file integrity
When uploading to Google Play Console, you'll need to provide the following metadata:
| Field | Value |
|---|---|
| App Name | Cambridge Beer Festival |
| Package Name | ralcock.cbf |
| Category | Food & Drink |
| Content Rating | 18+ (alcohol-related content) |
| Target Audience | Adults 18+ |
Browse beers, ciders, and more at the Cambridge Beer Festival
Cambridge Beer Festival App
The official companion app for the Cambridge Beer Festival, helping you discover and explore the incredible selection of drinks available at the festival.
🍺 FEATURES
• Browse hundreds of beers, ciders, perries, meads, and wines
• Search by name, brewery, or style
• Filter by category, style, and ABV
• Save your favorites for quick access
• Rate drinks to remember your preferences
• View detailed information about each drink
• Discover breweries and their complete product ranges
• Access festival information, dates, and location
• View the venue map
• Visit brewery websites directly
📋 DRINK CATEGORIES
• Real Ales & Craft Beers
• Ciders & Perries
• Meads
• International Beers
• Low & Non-Alcoholic Options
• Wines
🔍 SMART SEARCH & FILTERS
Find exactly what you're looking for with powerful search and filtering:
• Search across drink names, breweries, and styles
• Filter by ABV range (alcohol strength)
• Sort by name, brewery, ABV, or rating
• Browse by beer styles (IPA, Stout, Porter, Pale Ale, etc.)
• Quick category switching
⭐ PERSONALIZATION
• Save drinks to your favorites list
• Rate drinks from 1-5 stars
• Your preferences are saved locally on your device
• Favorites sync across festival editions
🌐 OFFLINE-FIRST DESIGN
The app caches festival data so you can browse even with limited connectivity at the venue.
📍 FESTIVAL INFORMATION
Access essential festival details:
• Event dates and times
• Venue location and map
• Official website link
• Festival updates
🎉 ABOUT CAMBRIDGE BEER FESTIVAL
The Cambridge Beer Festival is one of the UK's premier beer festivals, featuring an extensive selection of real ales, craft beers, ciders, and more from breweries across Britain and around the world.
---
This is an unofficial community app developed to enhance your festival experience. The app is not affiliated with or endorsed by the Cambridge Beer Festival organizers.
For official festival information, visit the Cambridge CAMRA website.
- Provided in the app (already configured)
- Format: PNG
- Size: 512x512 pixels
- Location:
android/app/src/main/res/mipmap-*
You need at least 2 screenshots for each supported device type:
Phone Screenshots (Required)
- Minimum 2, recommended 8
- Resolution: 16:9 or 9:16 aspect ratio
- Min size: 320px
- Max size: 3840px
7-inch Tablet Screenshots (Optional)
- Same requirements as phone
10-inch Tablet Screenshots (Optional)
- Same requirements as phone
Recommended Screenshots to Capture:
- Home screen with drink list
- Drink detail view
- Search and filters in action
- Favorites list
- Festival information screen
- Brewery detail view
- Category filter demonstration
- Star rating feature
- Size: 1024w × 500h pixels
- Format: PNG or JPEG
- Content: Should showcase the app name and main features
You must provide a privacy policy URL. The app collects:
- ✅ Local data only: Favorites and ratings (stored on device)
- ✅ Network requests: Fetches public festival data from API
- ❌ No personal information collected
- ❌ No analytics or tracking
- ❌ No ads
Privacy Policy Template:
Privacy Policy for Cambridge Beer Festival App
Last updated: [DATE]
This app does not collect, store, or transmit any personal information.
Data Storage:
- Favorite drinks and ratings are stored locally on your device
- No data is sent to external servers
- No analytics or tracking
Network Access:
- The app fetches public festival data from a Cloudflare Worker API
- No personal information is included in these requests
Permissions:
- Internet: Required to fetch festival data
- Storage: Required to save your favorites and ratings locally
Contact:
For questions about this privacy policy, please contact [YOUR EMAIL]
When you submit for content rating, answer:
- Does your app contain violence? No
- Does your app contain sexual content? No
- Does your app contain profanity or crude humor? No
- Does your app reference or depict drugs, alcohol, or tobacco? Yes (alcohol information)
- Does your app simulate gambling? No
This will result in:
- PEGI: 18 (Europe)
- ESRB: Mature 17+ (USA)
- USK: 18 (Germany)
- Rating varies by region (due to alcohol content)
- Target Age Group: 18+
- Age Restriction: Adults only (alcohol-related content)
- Google Play Rating: Will be rated based on content questionnaire
Before submitting to Play Store, ensure you have:
- App icon (512×512 PNG) - ✅ Already included
- Feature graphic (1024×500 PNG/JPEG)
- At least 2 phone screenshots
- Short description (80 chars max)
- Full description (4000 chars max)
- Privacy policy URL
- Content rating questionnaire completed
- App category selected (Food & Drink)
- Contact email address
- Target audience set (18+)
The Flutter app replaces an existing self-signed APK app with package name ralcock.cbf.
For existing users to receive it as an automatic update (not a reinstall), two things must hold:
- Package name matches —
applicationId = "ralcock.cbf"inbuild.gradle✅ already correct - Signing certificate matches — requires migrating to Play App Signing using the original keystore
Google Play requires AABs to be distributed via Play App Signing. During migration you upload your original signing key to Google; it becomes the app signing key that Google uses when delivering APKs to users. Devices that already have the app installed with the old certificate accept the update seamlessly because the distribution certificate hasn't changed.
Your original keystore also becomes the upload key used in CI — so there is nothing new to generate for the initial migration.
The new release's versionCode must be higher than whatever is currently live in Play Store.
Check pubspec.yaml — the build number after + is the version code (e.g. 2026.5.7+2026051707
→ versionCode = 2026051707). If the existing app's version code is higher, bump to a higher build
suffix before tagging.
- Open Google Play Console → your app
- Go to Release → Setup → App integrity (or App signing in older UI)
- Click App signing → Upgrade your app signing key (if present) or find the "App signing key" section
- Choose "Use a key exported from Java Keystore"
- Play Console provides a tool to encrypt and export your key — run it locally:
# Play Console shows you this exact command with the right parameters java -jar pepk.jar \ --keystore=your-original.jks \ --alias=your-key-alias \ --output=encrypted-key.zip \ --include-cert \ --encryptionkey=<hex-key-from-play-console>
- Upload the resulting
encrypted-key.zipto Play Console - Your original key is now enrolled as the app signing key — Google holds it and uses it to sign APKs delivered to users
You do not need to create a new keystore. Your original keystore becomes the upload key. If you ever need to rotate the upload key you can do so in Play Console without affecting users.
Set ANDROID_KEYSTORE_BASE64 to your original signing keystore (the same one you just
enrolled as the app signing key). CI will sign the AAB with it; Google will verify the signature
and re-sign the distributed APK/AAB with the same certificate existing users already have.
See github-secrets.md for the full secrets setup.
The Google Play API cannot upload to an app that has never had an AAB submitted. After completing the migration above:
- Push a tag to trigger the
Release Androidworkflow - When it completes, download the AAB from the GitHub Release
- In Play Console → Internal testing → Create new release → upload the AAB
- Complete and roll out the release
- From this point on, all future releases are uploaded automatically by CI
# 1. Update version in pubspec.yaml
version: 2026.5.7+2026051707
# 2. Commit and tag
git add pubspec.yaml
git commit -m "Bump version to v2026.5.0"
git tag -a v2026.5.0 -m "Release v2026.5.0"
git push origin main --follow-tagsCI builds → signs → uploads to Internal track. Promote to Production in Play Console.
When promoting a release in Play Console, paste the "What's Changed" section from the corresponding GitHub Release. For the English (UK) locale:
Version YYYY.MM.PATCH
[paste What's Changed from GitHub Release]
Releases are signed with an upload keystore. This is the standard Play App Signing model:
| Key | Held by | Purpose |
|---|---|---|
| App signing key | Google (Play App Signing) | Signs APKs delivered to users |
| Upload key | You (GitHub secret) | Signs the AAB you submit; Google verifies then re-signs |
If your upload key is ever compromised, you can rotate it in Play Console without affecting users.
android/app/build.gradle reads signing config from android/key.properties if that file exists.
In CI, the workflow writes key.properties from secrets before building. Locally, if key.properties
is absent the build falls back to debug signing (fine for development, not uploadable to Play).
This app replaces an existing Play Store app. Use the original signing keystore (the one used to sign all previous releases of
ralcock.cbf), not a newly generated one. Generating a new keystore would produce a different certificate and break the upgrade path for existing users. See the migration steps above for enrolling it in Play App Signing.
Base64-encode the original keystore for the GitHub secret:
# Linux/Mac
base64 -i original-signing.jks | tr -d '\n'
# Windows (PowerShell)
[Convert]::ToBase64String([IO.File]::ReadAllBytes("original-signing.jks"))Add these in Repository Settings → Secrets and variables → Actions:
| Secret | Value |
|---|---|
ANDROID_KEYSTORE_BASE64 |
Base64-encoded .jks content (from command above) |
ANDROID_KEY_ALIAS |
Alias used when creating the keystore (e.g. upload) |
ANDROID_KEY_PASSWORD |
Key password |
ANDROID_KEYSTORE_PASSWORD |
Keystore password |
See github-secrets.md for full secrets setup including Google Play.
android/key.properties and *.jks are gitignored. For local release builds with signing:
# android/key.properties (never commit this file)
storePassword=<password>
keyPassword=<password>
keyAlias=upload
storeFile=../../upload-keystore.jksCurrently, ProGuard/R8 is enabled (minifyEnabled = true). This reduces app size and generates a deobfuscation mapping file that is uploaded to two destinations automatically:
- Firebase Crashlytics — uploaded during the Gradle build by the
firebase-crashlytics-gradleplugin (uploadCrashlyticsMappingFileReleasetask). Crash reports in the Firebase Console are deobfuscated automatically. - Google Play Console — uploaded by the CI release workflow (
release-android.yml) after the AAB build. ANRs and crashes reported via Play Console are deobfuscated automatically.
Benefits:
- Smaller APK/AAB size: Removes unused code (~20-40% reduction)
- Code obfuscation: Makes reverse engineering harder
- Performance: Optimizes bytecode
Trade-offs:
- Build time: Adds 1-2 minutes to build
- Debugging: Stack traces need to be deobfuscated
- Compatibility: May break reflection-based code
1. Update build.gradle
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true // Enable code shrinking
shrinkResources true // Enable resource shrinking
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}2. Create proguard-rules.pro
Create android/app/proguard-rules.pro:
# Flutter wrapper classes
-keep class io.flutter.** { *; }
# Keep native methods
-keepclassmembers class * {
native <methods>;
}
# Preserve source file names and line numbers for actionable crash stack traces
-keepattributes SourceFile,LineNumberTable
-keepattributes Signature
-keepattributes *Annotation*
-keepattributes InnerClasses
-keepattributes EnclosingMethod
-dontwarn com.google.android.gms.**
-dontwarn androidx.lifecycle.DefaultLifecycleObserver
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
3. Test Thoroughly
# Build with ProGuard enabled
flutter build apk --release
# Install and test all features
adb install build/app/outputs/flutter-apk/app-release.apk
# Check app size reduction
ls -lh build/app/outputs/flutter-apk/app-release.apk4. Deobfuscate Stack Traces
If you encounter crashes in production, use the mapping file to deobfuscate:
# Mapping file location (after a release build)
build/app/outputs/mapping/release/mapping.txt
# Deobfuscate using the Android SDK retrace tool:
$ANDROID_HOME/tools/proguard/bin/retrace.sh -verbose \
build/app/outputs/mapping/release/mapping.txt obfuscated_trace.txtCurrent Status: ✅ ProGuard/R8 enabled
The mapping file is automatically uploaded to Google Play Console as part of the CI release workflow, enabling automatic deobfuscation of crash stack traces in the Play Console.
If you need to deobfuscate a stack trace locally:
# Mapping file location (after a release build)
build/app/outputs/mapping/release/mapping.txt
# Deobfuscate using the Android SDK retrace tool:
$ANDROID_HOME/tools/proguard/bin/retrace.sh -verbose \
build/app/outputs/mapping/release/mapping.txt obfuscated_trace.txt
# Or with a standalone retrace JAR:
java -jar $ANDROID_HOME/tools/proguard/lib/retrace.jar -verbose \
build/app/outputs/mapping/release/mapping.txt obfuscated_trace.txtNote: For AAB-based Play Store installs the mapping is also available in Play Console under Android Vitals → Deobfuscation files. For sideloaded APK installs, save the mapping file from CI artifacts within 7 days of the release build.
# Build locally
flutter build apk --release
# Install on device
adb install build/app/outputs/flutter-apk/app-release.apk
# Or share file and install via File ManagerYou cannot directly install AAB files. To test:
-
Use bundletool:
# Generate APKs from AAB bundletool build-apks --bundle=app-release.aab \ --output=app.apks \ --mode=universal # Extract universal APK unzip app.apks -d apks # Install adb install apks/universal.apk
-
Or upload to Internal Testing track in Play Console first
- App is already installed with a different signature
- Uninstall existing app first:
adb uninstall com.example.cambridge_beer_festival
- Ensure the
+YYYYMMDDPPbuild suffix inpubspec.yamlis higher than the current Play version code - Check
pubspec.yamlversion
- If replacing existing app, ensure package name matches exactly
- If new app, you cannot reuse a package name from deleted apps
- Once enrolled in Play App Signing, Google manages the signing
- You can upload unsigned AABs, and Google will sign them
# Check current version
flutter pub run flutter_version
# Build release APK locally
flutter build apk --release
# Build release AAB locally
flutter build appbundle --release
# Check APK size
ls -lh build/app/outputs/flutter-apk/app-release.apk
# Check AAB size
ls -lh build/app/outputs/bundle/release/app-release.aab
# Get SHA256 of APK
sha256sum build/app/outputs/flutter-apk/app-release.apk
# Install on device
adb install build/app/outputs/flutter-apk/app-release.apk
# View logs
adb logcat -s flutter
# Uninstall app
adb uninstall com.example.cambridge_beer_festival- Google Play Console
- Flutter Android Deployment
- Play App Signing
- Android App Bundle
- CalVer Specification
For issues with the release process, check:
- GitHub Actions workflow logs
- Google Play Console error messages
- Flutter build output
For app issues, file a GitHub issue or contact the development team.