Deploying Open Source Pull Requests to Firebase Hosting

3 minute read

The Problem

I volunteer at Code for Boston. I help out with web app development, contributing code and reviewing contributions. All of our projects are open source, run on Github, and follow the fork-and-pull development model.

We built my last two projects using Next.js on Firebase Hosting, and our CI runs on Github Actions. Firebase provides some great tooling: firebase init can set up workflows for CD off of a specific branch and preview deployments for pull requests.

Unfortunately, the preview deployment workflow does not work across forks. Workflows that run on pull requests from forks do not have access to Github secrets. Otherwise, a malicious user could craft a pull request to leak secrets by printing them out. Since we can’t secrets, we can’t provide Firebase credentials to the PR workflow.

Without automated deployments for pull requests, reviewers have to check out the pull request branch and run the site themselves. This can be a significant barrier to participation, especially for non-developers looking at UI.

The Solution

We automate PR deploys by splitting the workflow into two. We build the application in the PR workflow, store the static files as an artifact, and deploy it using a second workflow triggered using the workflow_run action trigger. This trigger allows running a workflow after the completion of another specified workflow. The triggered workflow always runs at the tip of the default branch. This way, it runs with trusted code, and so has access to secrets.

Building The Site

This workflow runs on each pull request, building the site and uploading to an artifact:

name: Build Pull Request

on: [pull_request]

jobs:
  build:
    # Don't build forks, even if actions are enabled
    if: github.repository_owner == 'codeforboston'
    name: Build
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Code
        uses: actions/checkout@v2
      # Set up the build environment using a local composite action
      - name: Setup Build Environment
        uses: ./.github/actions/setup-repo
      # Build the app, putting the static files in ./out/
      - name: Build App
        run: yarn export:nolint
      # Upload the static files to an artifact
      - name: Export App
        uses: actions/upload-artifact@v2
        if: github.event_name == 'pull_request'
        with:
          name: digital-testimony-app
          path: out
          retention-days: 1

Deploying The Site

This workflow deploys the artifact uploaded by the previous workflow to a preview URL and posts it to the PR conversation for review:

name: Deploy Pull Request

on:
  workflow_run:
    workflows: ["Build Pull Request"]
    types:
      - completed

jobs:
  deploy:
    name: Deploy Pull Request
    runs-on: ubuntu-latest
    # Don't build forks, even if actions are enabled
    if: github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'codeforboston'
    environment: dev

    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      # Download the site's static files and deploy them to Firebase Hosting
      - name: Deploy App to Firebase Hosting
        uses: alexjball/action-hosting-deploy@v1
        with:
          repoToken: "$"
          firebaseServiceAccount: "$"
          projectId: digital-testimony-dev
          artifactName: digital-testimony-app

Note that this workflow runs on the default branch, and so uses the Firebase configuration from that branch. Only the content of the site is affected by the PR.

We reused the Firebase deployment action, patching it to accept the artifactName of the site artifact uploaded in the triggering workflow. The action can resolve the artifact using the Github API.

You can generate a service account for firebaseServiceAccount using firebase init. Just ignore the workflow files it generates.

Is This Safe?

This process deploys external contributor code. Can this be abused? Not really, at least not any more than one could with a local development frontend, which is publicly accessible. Both use development client keys, and both are allowed domains on our Firebase projects. And the development environment contains no private data, so phishing-styles attacks are not an issue.

Conclusion

Splitting the build and deploy steps between workflows using the workflow_run trigger allowed us to easily deploy pull requests to preview URL’s. This enables more people to participate in reviews and makes it easy to review frontend changes without checking out the PR branch.

Updated: