Firebase preview channels and GitLab CI/CD

October 29, 2020

Recently a new feature called preview channels was released for Firebase Hosting. Since tensor5.dev is hosted on Firebase, I decided to try this new feature, and now it’s a good time to share the pipeline that I use to build, test and deploy this website, which is made with Gatsby, with source code hosted on GitLab, and uses GitLab CI/CD for continuous integration and deployment; in particular, I will focus on how to do continuous deployment using Firebase preview channels. My pipeline is for Gatsby, but it’s generic enough that it could work with any other Node.js based static site generator.

Preview channels are a new feature of Firebase Hosting that allow you to deploy your website to a temporary URL. When you host a website on Firebase it is normally available at:

https://<project_id>.web.app
https://<project_id>.firebaseapp.com

plus any custom domain that you set up; these are called the live channels. With preview channels you can deploy at URLs:

https://<project_id>--<channel_id>-<random_hash>.web.app
https://<project_id>--<channel_id>-<random_hash>.firebaseapp.com

where channel_id is some name that you choose for your preview channel, while random_hash is generated by Firebase and is used to distinguish different generations of your deployment. Preview channels are useful for example if you want to see how the production version of your website will look like before you actually deploy it to its final URL, or if you have multiple collaborators sending pull requests (aka merge requests in GitLab) to your Git repository and you want to review the final deployed version of their changes before merging them to master, which will then go live.

Let’s see how this can be done with GitLab. Assuming that you are familiar with the basics of GitLab CI/CD configuration, this is a simplified version of my .gitlab.yml:

default:
  image: node:alpine
  before_script:
    - yarn --frozen-lockfile

build:
  stage: build
  script: yarn build
  artifacts:
    paths:
      - public
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

test:
  stage: test
  script: yarn test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

.deploy:
  stage: deploy
  before_script:
    - npm install -g firebase-tools

deploy preview:
  extends: .deploy
  script: firebase --project "${FIREBASE_PROJECT_ID}" --token "${FIREBASE_TOKEN}" hosting:channel:deploy "${CI_COMMIT_SHA}"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

deploy live:
  extends: .deploy
  script: firebase --project "${FIREBASE_PROJECT_ID}" --token "${FIREBASE_TOKEN}" hosting:clone "${FIREBASE_PROJECT_ID}:${CI_COMMIT_SHA}" "${FIREBASE_PROJECT_ID}:live"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Except for the last one, all jobs described above run only on commits associated to merge requests, that’s what the rules if: $CI_PIPELINE_SOURCE == "merge_request_event" mean (read more here).

A few things are needed in order for this pipeline to work correctly. First, the firebase command that appears in the deploy jobs requires a firebase.json configuration file in the root of your project; you can generate one by running firebase init hosting (more here), what matters is that the public root directory (hosting.public in the firebase.json) matches the output directory of your static site gererator, i.e. the artifact of your build job; in my example Gatsby uses public as output directory, hence my build job has the path public specified as artifact.

Second, you need to add a couple of custom environment variables to your GitLab project. Go to Settings > CI/CD and expand the Variables section. Add the variable FIREBASE_PROJECT_ID holding your Firebase project ID; uncheck the Protect variable option or else the variable will only be visible on protected branches and not on merge requests. Next generate an access token with firebase login:ci and add a variable FIREBASE_TOKEN holding it; again uncheck Protect variable, but this time check the Mask variable option so that the token does not appear in job logs.

Third, in the Settings > General of your GitLab project expand the Merge requests section and set the Merge method to Fast-forward merge; we will see later why this is needed.

During the deploy stage of a merge request, the deploy preview job will deploy the website from the public directory (as specified in firebase.json) to a preview channel; to make sure that each merge request has its own unique channel we use the commit SHA as channel ID. The command firebase hosting:channel:list will show you the list of deployed preview channels: find the commit SHA in the Channel ID column and follow the corresponding link to review your website.

Once the merge request is approved and merged to master, the deploy live job clones the content of the preview channel to the live channel. This works because we use fast-forward merges: the CI_COMMIT_SHA variable that appears in the deploy preview and deploy live jobs has the same value since the merge request commit and the master branch commit are equal.

Now we have a flow where we can review a pull request, see how the deployed version of our website looks like on the browser, and be sure that once we merge it to master the exact same version of the website will go live.


© 2020, Nicola Squartini