This guide explains how to set up all GitHub Secrets required by CI/CD workflows.
Several configuration files are excluded from version control for security. GitHub Secrets allow CI/CD workflows to recreate these files at build time.
You need to add 7 secrets to your GitHub repository:
Firebase (existing):
GOOGLE_SERVICES_JSON- Android Firebase configurationFIREBASE_OPTIONS_DART- Flutter Firebase configuration
Android signing (new):
ANDROID_KEYSTORE_BASE64- Base64-encoded upload keystoreANDROID_KEY_ALIAS- Key alias inside the keystoreANDROID_KEY_PASSWORD- Key passwordANDROID_KEYSTORE_PASSWORD- Keystore (store) password
Google Play (new):
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON- Service account JSON for automated Play uploads
Before setting up GitHub Secrets, you must complete the Firebase setup on your local machine:
- Follow the instructions in
FIREBASE_SETUP.md - Create a Firebase project
- Download
google-services.jsontoandroid/app/ - Run
flutterfire configureto generatelib/firebase_options.dart - Verify the app builds locally with Firebase
Get the content:
# From project root, copy the entire file content
cat android/app/google-services.jsonCopy the entire JSON output. It should look like this:
{
"project_info": {
"project_number": "123456789012",
"project_id": "your-firebase-project",
"storage_bucket": "your-firebase-project.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:123456789012:android:abcdef1234567890",
"android_client_info": {
"package_name": "ralcock.cbf"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}Get the content:
# From project root, copy the entire file content
cat lib/firebase_options.dartCopy the entire Dart file content. It should look like this:
// File generated by FlutterFire CLI.
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
// ... more platforms
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
appId: '1:123456789012:web:abcdef1234567890',
// ... more config
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
appId: '1:123456789012:android:abcdef1234567890',
// ... more config
);
}-
Navigate to your repository on GitHub
- Go to:
https://github.com/YOUR_USERNAME/cambridge-beer-festival-app
- Go to:
-
Open Settings
- Click Settings tab (requires admin access)
-
Navigate to Secrets
- In the left sidebar: Secrets and variables → Actions
-
Add First Secret
- Click New repository secret
- Name:
GOOGLE_SERVICES_JSON - Value: Paste the entire content from
android/app/google-services.json - Click Add secret
-
Add Second Secret
- Click New repository secret
- Name:
FIREBASE_OPTIONS_DART - Value: Paste the entire content from
lib/firebase_options.dart - Click Add secret
# Install GitHub CLI if not already installed
# macOS: brew install gh
# Other: https://cli.github.com/
# Authenticate
gh auth login
# Add GOOGLE_SERVICES_JSON secret
gh secret set GOOGLE_SERVICES_JSON < android/app/google-services.json
# Add FIREBASE_OPTIONS_DART secret
gh secret set FIREBASE_OPTIONS_DART < lib/firebase_options.dart- Go to: Settings → Secrets and variables → Actions
- You should see both secrets listed:
GOOGLE_SERVICES_JSONFIREBASE_OPTIONS_DART
gh secret listExpected output:
CLOUDFLARE_API_TOKEN Updated 2024-XX-XX
CODECOV_TOKEN Updated 2024-XX-XX
FIREBASE_OPTIONS_DART Updated 2024-XX-XX
GOOGLE_SERVICES_JSON Updated 2024-XX-XX
Push a change to trigger the workflow:
# Make a small change
echo "# Test" >> README.md
# Commit and push
git add README.md
git commit -m "Test Firebase CI/CD"
git pushMonitor the workflow:
- Go to your repository on GitHub
- Click Actions tab
- Click on the running workflow
- Check that these steps succeed:
- "Create Firebase google-services.json"
- "Create Firebase options"
- "Get dependencies"
- "Run tests"
- "Build web" / "Build Android"
The GitHub Actions workflows include these steps:
Flutter App CI/CD (.github/workflows/ci.yml):
- name: Create Firebase google-services.json
run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/google-services.json
- name: Create Firebase options
run: echo '${{ secrets.FIREBASE_OPTIONS_DART }}' > lib/firebase_options.dartThese steps:
- Read the secret value from GitHub Secrets
- Write it to the expected file location
- Allow subsequent build steps to access the files
- Keep secrets in GitHub Secrets - Never commit them to the repository
- Restrict repository access - Only trusted collaborators should have admin access
- Use environment-specific secrets - Different secrets for staging/production
- Rotate secrets periodically - Update Firebase config if compromised
- Enable branch protection - Require reviews for changes to main branch
- Don't commit secrets to Git - They're in
.gitignorefor a reason - Don't share secret values - Send setup instructions instead
- Don't use secrets in fork PRs - GitHub doesn't expose secrets to forks
- Don't log secret values - Be careful with debug output
Error: secret GOOGLE_SERVICES_JSON not found
Solution:
- Verify secret name is exactly
GOOGLE_SERVICES_JSON(case-sensitive) - Check you added it as a repository secret, not environment secret
- Ensure you have admin access to the repository
Error: Error parsing google-services.json
Solution:
- Verify the secret contains valid JSON
- Check for trailing commas or syntax errors
- Re-download from Firebase Console if needed
- Use a JSON validator: https://jsonlint.com/
Solution:
- Verify both secrets are set correctly
- Check workflow logs for file creation steps
- Ensure
flutterfire configuregenerated correct config - Verify secret contains complete file content
This is expected GitHub behavior. Secrets are not exposed to workflows triggered by forks for security reasons.
Solution:
- Contributors must set up their own Firebase project and secrets
- Or run tests locally before submitting PR
- Or maintainer merges to a branch in the main repo to trigger CI
If you need to update Firebase configuration:
# After downloading new google-services.json from Firebase Console
gh secret set GOOGLE_SERVICES_JSON < android/app/google-services.jsonOr via web interface:
- Settings → Secrets and variables → Actions
- Click Update next to
GOOGLE_SERVICES_JSON - Paste new value
- Click Update secret
# After running flutterfire configure
gh secret set FIREBASE_OPTIONS_DART < lib/firebase_options.dart- Firebase Setup Guide - Complete Firebase setup instructions
- GitHub Encrypted Secrets
- GitHub Actions Security
# List all secrets
gh secret list
# Set/update a secret from file
gh secret set SECRET_NAME < path/to/file
# Set/update a secret from stdin
echo "secret value" | gh secret set SECRET_NAME
# Delete a secret
gh secret delete SECRET_NAMEIf you encounter issues:
- Check the Firebase Setup Guide
- Verify secrets are set correctly in GitHub Settings
- Check GitHub Actions logs for specific error messages
- Ensure local Firebase setup works before adding to CI/CD
- Review this guide's troubleshooting section
These secrets are required by .github/workflows/release-android.yml to sign the APK and AAB
with the upload keystore before uploading to Google Play.
| Key | Held by | Purpose |
|---|---|---|
| App signing key | Signs APKs delivered to users | |
| Upload key | You (these secrets) | Signs the AAB you submit; Google verifies then re-signs |
Storing only the upload key in CI means the real distribution key is never exposed. If the upload key is compromised you can rotate it in Play Console without affecting users.
Replacing an existing Play Store app? Use the original signing keystore — the same one that was used to sign previous releases. Do NOT generate a new keystore. The original keystore must first be enrolled in Play App Signing (see android-release.md). Enrolling it as the app signing key is what ensures existing users receive the update seamlessly. After enrollment, the same keystore serves as the upload key in CI.
For a brand new app with no existing Play Store history, generate a keystore:
keytool -genkey -v -keystore upload-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias upload# Linux/Mac
base64 -i upload-keystore.jks | tr -d '\n'
# Windows (PowerShell)
[Convert]::ToBase64String([IO.File]::ReadAllBytes("upload-keystore.jks"))Go to: Repository Settings → Secrets and variables → Actions → New repository secret
| Secret | Value |
|---|---|
ANDROID_KEYSTORE_BASE64 |
The full base64 string from step 2 |
ANDROID_KEY_ALIAS |
The alias used in keytool (e.g. upload) |
ANDROID_KEY_PASSWORD |
The key password entered in keytool |
ANDROID_KEYSTORE_PASSWORD |
The keystore password entered in keytool |
Via GitHub CLI:
# Encode and set in one step
base64 -i upload-keystore.jks | tr -d '\n' | gh secret set ANDROID_KEYSTORE_BASE64
echo -n "your-key-alias" | gh secret set ANDROID_KEY_ALIAS
echo -n "your-key-password" | gh secret set ANDROID_KEY_PASSWORD
echo -n "your-store-password" | gh secret set ANDROID_KEYSTORE_PASSWORDThis secret is required by the publish-google-play job in release-android.yml to upload
the signed AAB to the Internal track automatically on every release.
- Open Google Cloud Console and select (or create) the project linked to your Play Console account
- Go to APIs & Services → Library
- Search for Google Play Android Developer API and click Enable
- Go to APIs & Services → Credentials → Create Credentials → Service account
- Name it (e.g.
github-play-publisher), click Create and continue - Skip optional role assignment, click Done
- Click the service account → Keys tab → Add key → Create new key → JSON
- Download the JSON file — this is your secret value
- Open Google Play Console
- Go to Setup → API access
- Click Link next to the Google Cloud project from step 1 (if not already linked)
- Under Service accounts, find the account you created and click Grant access
- Set permissions to Release manager (or at minimum Release to Internal testing)
- Click Apply and Invite user
gh secret set GOOGLE_PLAY_SERVICE_ACCOUNT_JSON < path/to/service-account.jsonOr via the web interface: paste the entire JSON file content as the secret value.
Every push of a v* tag will automatically:
- Build the signed AAB and create a GitHub Release
- Attempt to upload the AAB to the Internal track in Play Console
First release only: The Play upload step will be skipped until the app has been submitted to Play Console at least once manually (the API cannot create a new listing). The workflow will still succeed — the GitHub Release with the signed AAB is always created. See the first-upload steps in android-release.md. From the second release onwards, the AAB appears in Internal testing automatically.
From there you manually promote to Alpha → Beta → Production in Play Console.
"Permission denied" or "403 Forbidden"
- Verify the service account was granted access in Play Console (step 3)
- Check the app is published at least once manually before the API can upload updates
"Package not found"
- Ensure the
packageNamein the workflow (ralcock.cbf) matches the app in Play Console exactly
"Upload key certificate does not match"
- The upload keystore registered in Play Console must match the one in
ANDROID_KEYSTORE_BASE64 - On first upload, Play Console registers your upload certificate automatically