I recently set up a GitHub action to deploy Firebase after pull request merge. It's a tremendous time-saver. Previously, I was deploying from my dev machine, doing some toil to switch between a development environment (emulators) and the production environment.
Project setup
I use environment variables to control the various Firebase variables (project/app ID, API key, etc). Note that the Firebase API key is not a secret key. I put these in .envrc
on my local machine but the action needs a bit more help setting up the environment.
I have a script that uses jq
to create a JSON file from a template. For example, write-config.sh
#!/bin/bash
# Write the overall firebase config:
jq -n \
--arg FIREBASE_PROJECT_ID "$FIREBASE_PROJECT_ID" \
-f .firebaserc.jq \
> .firebaserc
# Write the json file loaded by the kotlin-angular build:
jq -n \
--arg FIREBASE_PROJECT_ID "$FIREBASE_PROJECT_ID" \
--arg FIREBASE_APP_ID "$FIREBASE_APP_ID" \
--arg FIREBASE_STORAGE_BUCKET "$FIREBASE_STORAGE_BUCKET" \
--arg FIREBASE_API_KEY "$FIREBASE_API_KEY" \
--arg FIREBASE_AUTH_DOMAIN "$FIREBASE_AUTH_DOMAIN" \
--arg FIREBASE_MESSAGING_SENDER_ID "$FIREBASE_MESSAGING_SENDER_ID" \
--arg FIREBASE_USE_EMULATORS "$FIREBASE_USE_EMULATORS" \
-f webApp/src/jsMain/resources/firebase-config.json.jq \
> webApp/src/jsMain/resources/firebase-config.json
The template files are quite simple, here's one for firebase.json
(used by the CLI):
{
"projects": {
"default": "\($FIREBASE_PROJECT_ID)"
}
}
You also need your Firebase environment configured in the client. This particular project builds Angular via gradle (it's a long story, see also Kotlin in the Browser). But I used the same json format that Angular Fire recommends. Here's the firebase-config.json.jq
template:
{
"projectId": "\($FIREBASE_PROJECT_ID)",
"appId": "\($FIREBASE_APP_ID)",
"apiKey": "\($FIREBASE_API_KEY)",
"authDomain": "\($FIREBASE_AUTH_DOMAIN)",
"storageBucket": "\($FIREBASE_STORAGE_BUCKET)",
"messagingSenderId": "\($FIREBASE_MESSAGING_SENDER_ID)",
"useEmulators": "\($FIREBASE_USE_EMULATORS)"
}
Repository setup
Set up a target environment (eg "Production") and populate it with values from the Firebase console:
FIREBASE_PROJECT_ID
FIREBASE_APP_ID
FIREBASE_STORAGE_BUCKET
FIREBASE_API_KEY
FIREBASE_AUTH_DOMAIN
FIREBASE_MESSAGING_SENDER_ID
Also, create a secret named FIREBASE_SERVICE_ACCOUNT_BASE64
containing a newly exported json
service account key (see below).
GitHub action
The action is a straightforward series of commands,
- check out the repo
- Generate config (as above)
- Install Firebase CLI
- Build the app with gradle
- Deploy web app to Hosting
- Deploy Firestore rules
- Clean up
Services deployed
The following Firebase services are deployed:
- Hosting
- Provides the main web app
- Firestore (rules)
- Defines the database security rules
Service account & permissions required
Create a new service account in the GCP IAM console panel. Call it something like "GitHub deploy", and only use it for GitHub action deploys.
The permissions are a bit trickier. The easy way out is to make the service account an overall admin, but consider following the principle of least privilege. Limit the impact of a malicious or mistaken actor.
Through trial and error I think this is it:
- Firebase Hosting Admin
- Needed to deploy to Hosting
- Firebase Rules Admin
- Needed to deploy Rules (for Firestore)
- Service Account User
- Needed to act as the service account
- Service Usage Consumer
- Needed to test if APIs are active
Full YAML source
name: Deploy to Firebase on merge
on:
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: './.nvmrc'
- name: Generate .firebaserc
run: |
./write-firebase-config.sh
cat .firebaserc
env:
FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }}
FIREBASE_APP_ID: ${{ vars.FIREBASE_APP_ID }}
FIREBASE_STORAGE_BUCKET: ${{ vars.FIREBASE_STORAGE_BUCKET }}
FIREBASE_API_KEY: ${{ vars.FIREBASE_API_KEY }}
FIREBASE_AUTH_DOMAIN: ${{ vars.FIREBASE_AUTH_DOMAIN }}
FIREBASE_MESSAGING_SENDER_ID: ${{ vars.FIREBASE_MESSAGING_SENDER_ID }}
FIREBASE_USE_EMULATORS: false
- name: Install Firebase CLI
run: |
npm install -g firebase-tools
firebase --version
- name: Build Angular app
run: |
./gradlew webApp:buildProductionWebApp
- name: Deploy hosting
run: |
echo "${{ secrets.FIREBASE_SERVICE_ACCOUNT_BASE64 }}" | base64 --decode > "google-application-credentials.json"
firebase deploy --only hosting --non-interactive
rm -rf "google-application-credentials.json"
env:
GOOGLE_APPLICATION_CREDENTIALS: "google-application-credentials.json"
- name: Deploy Firestore rules
run: |
echo "${{ secrets.FIREBASE_SERVICE_ACCOUNT_BASE64 }}" | base64 --decode > "google-application-credentials.json"
firebase deploy --only firestore:rules --non-interactive
rm -rf "google-application-credentials.json"
env:
GOOGLE_APPLICATION_CREDENTIALS: "google-application-credentials.json"
- name: Cleanup credentials
if: always()
run: |
rm -rf "google-application-credentials.json"
Happy Firebaseing!!
Top comments (0)