DEV Community

Cover image for Deploying to Firebase Hosting + Firestore from GitHub actions
David Haley
David Haley

Posted on

Deploying to Firebase Hosting + Firestore from GitHub actions

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
Enter fullscreen mode Exit fullscreen mode

The template files are quite simple, here's one for firebase.json (used by the CLI):

{
  "projects": {
    "default": "\($FIREBASE_PROJECT_ID)"
  }
}
Enter fullscreen mode Exit fullscreen mode

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)"
}
Enter fullscreen mode Exit fullscreen mode

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,

  1. check out the repo
  2. Generate config (as above)
  3. Install Firebase CLI
  4. Build the app with gradle
  5. Deploy web app to Hosting
  6. Deploy Firestore rules
  7. 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"
Enter fullscreen mode Exit fullscreen mode

Happy Firebaseing!!

Top comments (0)