CICD for frontend with Firebase and GitHub Actions

Theviyanthan Krishnamohan
Level Up Coding
Published in
11 min readAug 9, 2020

--

GitHub Actions has made CICD easy. Couple it with the free hosting service offered by Firebase, you have a fully functional CICD pipeline ready for your frontend app completely free in no time.

As frontend developers, we may often have to demonstrate our app to the rest of the team to get their two cents on the design and the user experience of the app. However, just giving them a walkthrough of your app using a projector or a Zoom meeting might prove to be insufficient.

After all, unless you play around with the app yourself, it will be difficult to form qualified opinions. Having a CICD pipeline set up to continuously push your changes to a working app on the web will come in handy here.

What GitHub Actions allows you to do is to set up a CICD pipeline with just a few yaml files. What's more, GitHub Actions is a part of GitHub and you don't have to use external tools to set up CICD.

The pipeline

So, let’s get into the thick of things and deploy our CICD pipeline. This is what we will be doing.

  1. Create a React-based frontend app
  2. Use GitHub Actions to automate the process of deployment

Our app will be deployed in two different environments, namely, staging and production. When a pull request is created, the app should be built to check if there are any build errors. Once the pull request is merged, then, once again, the app should be built and then deployed to the staging environment. Once its deployed, the patch version of the app should be bumped.

When a pre-release version of the app is released, the app should be built and deployed to the production environment. When the final release happens, the app should be built and deployed to the production environment, after which the minor version of the app should be bumped.

Creating a React app

First, let’s create a React app. I am going to bootstrap a simple app using the Create React App environment using the following command.

npx create-react-app my-app

This is a simple React app which we shall be deploying to the web using GitHub Actions. Before we set up the pipeline, we need to create the two environments.

Installing Firebase tools

For hosting the app on the web, I decided to use Firebase here. But you can try your luck with other free hosting options such as Heroku too.

Firebase gives us a CLI app that helps us publish our app to Firebase a lot easily. To use that, let’s install it first.

npm install -g firebase-tools

Once installed, we will have to log in to our Google account. Since Firebase is a service offered by Google, we need a Google account to access their services. Use the following command to login.

firebase login

Once you enter this command, your browser will open with the Google login form. Enter your credentials and grant the Firebase CLI the necessary permissions.

Initializing a Firebase project

Then, we can initiate our app with Firebase by using the following command.

firebase init

Once you enter this command, you will be greeted with the following screen.

Firebase offers a lot of services ranging from web hosting to a real-time no-SQL database. Since we only want to host our web app, we need to choose Hosting from the list of services displayed. On selecting it, we will be taken to the next screen.

Here, we will have to either choose a project or create a new project. Since we don’t have a project, we need to choose the Create a new project option. Once selected, you will be asked to provide a unique id for the project. Provide one. Next, you will be asked to enter a name for the project. Enter a name and hit enter.

The CLI might take a few seconds to create the project. Once done, you will be asked to set the public directory. The public directory is the directory from which the static files should be served. Mostly, it is the directory into which the bundler bundles the JavaScript files. Since Create React App outputs the files into the "build" directory, that should be our public directory.

So, enter build and hit enter.

In the next step, you will be asked if you want to rewrite all URLs to index.html. Since ours is a Single Page Application, we need that. So, say yes to complete the initialization process.

Creating a Firebase site

Once completed, we have to go to the Firebase console to create an additional site. When creating a project for hosting, Firebase will have already created a site. However, since we need two sites-one for each staging and production, we need to create an additional site.

To do that, go to https://console.firebase.google.com/ and select the project that you created just now. Then, select Hosting from the side panel. You may have to click on Get Started and go through step-by-step instructions to arrive at the console view.

In the console view, close to the bottom of the view, you will have a button called Add another site. Click on it to create an additional site. We will be using this site for staging so let’s call it “sample-staging”. Once added, the new site should appear under the domains section.

Firebase offers both “web.app” and “firebaseapp.com” domains, so you will see two domains for each site that you have. At this point, it’s better to make a note of the name of the already existing site.

Deploying to multiple sites

Now, let’s move back to our app. Since we have two apps, we, now, need to tell Firebase which site to deploy our app to. We can do this by using target names and then mapping the target name to a site.

To set a target name, go to the firebase.json file at the root directory. There you will find that the hosting attribute has an object assigned to it. We need to replace it with an array of objects. To that end, copy the object and pass it into an array. Then, duplicate the object to make the array have two objects. Create a key called target in both the objects, and name one "staging" and the other "production".

At the end, you should have something like this.

{ 
"hosting": [
{
"target": "staging",
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
},
{
"target": "production",
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
} ]
}

Now, we need to map these targets to the right sites. To do that let’s use the following command.

firebase target:apply <service> <target> <site>

Service refers to the different services offered by Firebase. Since the service that we use here is hosting, it should be hosting. The target refers to the target name that we defined in the firebase.json file. The site is the name of the site we want the target to be mapped to.

We need to map the staging target to our staging site. So, use the following command.

firebase target:apply hosting staging staging-github-actions

Then, let’s map the production target to the production site.

firebase target:apply hosting production production-github-actions

Once done, we can deploy our app to Firebase by using the following command. But we won’t be doing it locally since we want the app to be deployed by GitHub Actions.

firebase deploy --only hosting:staging

Similarly, we can use this command to deploy to production:

firebase deploy --only hosting:production

Generating a Firebase token

Remember, our app should be deployed to Firebase from GitHub Actions. What GitHub Actions does is to run containers in the cloud that would carry out the deployment process as per our configuration. So, when deploying from a container, we won’t be able to go through the usual login flow. Mind you, the deployment process is fully automated so you can’t intervene in any way.

Hence, there should be another way to authenticate ourself with Firebase. Firebase helps us by providing us with tokens which we can use to let GitHub Actions login on our behalf. So, let’s create a Firebase token by entering the following command.

firebase login:ci

You may be asked to login again. Once you login and grant the necessary permissions, the token should appear on the terminal. Make a copy of it.

This completes the app-level configuration. Now, commit everything and push them to a GitHub repo. Now, let’s move onto GitHub.

GitHub Secrets

First, we need to store our Firebase token safely. We can paste it directly onto the configuration files but that is dangerous since the token would become public. Instead, we should use the secrets option provided by GitHub to store secrets safely. In your repo, go to the “settings” tab and select “secrets” from the vertical menu.

On the secrets page, click on the New Secret button. Provide a name for the secret and paste the token that we just copied into the Value textarea and click on Add secret. I used FIREBASE_TOKEN as the name.

Create a Personal Access Token

To create a personal access token, go to https://github.com/settings/profile and click on Developer settings at the bottom of the menu. Then, select Personal access tokens from the menu and click on Generate new token. You may have to provide your password here. Provide a name for the token using the Note textbox and select the public_repo scope under Repo. This is the only scope we need.

Once done, copy the token and store it in GitHub secrets as we did previously with the Firebase token. It is important to note that you cannot read this token once again.

Configuring GitHub Actions

Now, it’s time to configure GitHub Actions. Click on the Actions tab on your GitHub repo and select “Node.js” from the list of workflows shown.

on: push: branches: [ master ] pull_request: branches: [ master ]

In jobs->build->strategy->matrix, we can specify the versions of node that build should run on. By default, 10.x, 12.x, 14.x will be selected. When three versions are selected, GitHub Actions runs the action thrice concurrently. Since we also want to deploy our app to Firebase, this will also deploy it thrice concurrently causing an unpredictable behavior. So, it's advisable to select only one version. I decided to go with 10.x.

strategy: matrix: node-version: [10.x]

Adding steps to GitHub Actions

In the boilerplate, you will see [email protected] and [email protected] being used. These are actions that are used to check out the branch of a repo, and install node respectively. More such actions can be found in the marketplace.

Configuring staging using GitHub Actions

These are the steps we need to execute during staging.

If you observe the boilerplate, the first one would have been already configured. But it would be a part of the step that sets up node. Let’s spin it off into a separate step. So, create a step by using the hyphen and then set the name to Build Project. The run attribute is used to run commands. You can run multiple commands by using the pipe (|) operator and entering the commands in the lines below it.

- name: Build Project run: | npm ci npm run build --if-present npm test

Deploying to Firebase from GitHub Actions

Let’s create the next step now. We will have to install firebase tools, and then deploy the app to Firebase. To be able to deploy, we should authenticate first. To do that, we need to use the token we stored in GitHub secrets. GitHub secrets can be accessed in the following way.

${{secrets.FIREBASE_TOKEN}}

Here, secrets are followed by the name of the secret. We need to set the secret to an environment variable. We can do that using the env attribute. Then, we can use the token flag to refer to this environment variable when deploying.

- name: Install Firebase CLI 
env: FIREBASE_TOKEN: ${{secrets.FIREBASE_TOKEN}}
run: |
sudo npm install -g firebase-tools
firebase deploy --token $FIREBASE_TOKEN --only hosting:staging --non-interactive

However, we want our app to be deployed only when a pull request is merged. But this action is triggered both when a pull request is raised and merged. We can run this step conditionally, using the if attribute.

if: github.event_name=='push'

This runs this step only if the event is a push. It is worth noting that when a pull request is merged, we are actually pushing the merge commit to the repo.

Bumping the version

Now, let’s bump the version. Since git is already installed, we just need to configure git and bump the version. But to be able to push the commit, we need to have been authenticated. We can authenticate ourselves using the personal access token.

The first step in our configuration checkouts our repo using the [email protected] action. You can pass the token using the attribute to have the action log in to our repo before checking out.

- uses: actions/checkout@v2 
with: token: ${{secrets.PERSONAL_ACCESS_TOKEN}}]

Use the git config command to configure the user name and email. Then, bump the patch version using the npm version command.

npm version patch

This command will bump the version and commit it to the repo. So, now, we need to push it.

This should also be run only when a pull request is merged. So, let’s use the same condition here as well.

- name: Version Bumping 
run: |
git config --global user.email "checout@v2"
git config -- global user.name "Version Bumping"
npm version patch git push
if: github.event_name=='push'

Ignoring paths

Now, this causes a new problem. This action is triggered every time something is pushed to the master branch. When we bump the version and push the commit, this will trigger the action once again. So, we will end up in an endless cycle of actions.

Use the paths-ignore attribute under on and push to ignore these files.

on: 
push:
branches:
- master
paths-ignore:
- 'package.json'
- 'package-lock.json'

Our staging configurations are complete. Let’s save the file and create a new workflow for production. Once saved, you can find the configuration file in the .github/workflows directory at the root.

Configuring production using GitHub Actions

For production, the action should be triggered when the app is pre-released and released. So, under the on attribute, let's use the release attribute and specify both released and prereleased as values.

on: release: types: [released, prereleased]

We can follow the same steps to configure the deployment to Firebase. You will only have to change the site name and set the target to production.

- name: Install Firebase CLI 
env: FIREBASE_TOKEN: ${{secrets.FIREBASE_TOKEN}}
run: |
sudo npm install -g firebase-tools
firebase deploy --token $FIREBASE_TOKEN --only hosting:production --non-interactive

Version bumping also follows similar steps. However, since the action is triggered on release, the [email protected] action checkouts a ref instead of a branch. So, we won't be able to push our changes. To prevent this, during the version bumping step, we need to force checkout the master branch before pushing the commit. Before that, it is better to update the remote repos and get a fetch.

- name: Version Bumping 
run: |
git config --global user.email "[email protected]"
git config -- global user.name "Version Bumping"
git remote update
git fetch
git checkout --progress --force -B master refs/remotes/origin/master
npm version minor
git push
if: github.event.action=='released'

This ends the production configuration. Now, you can save it and test if the flows are working as intended. You can find a sample repository configured with GitHub Actions .

There you are! Now, all that we need to do is to merge pull requests and release our app to get it to deploy to staging and production. With minimum effort, now we have a fully functioning CICD flow. What is more, our team can now follow our work in real-time and provide the much-needed feedback.

Originally published at https://www.thearmchaircritic.org on August 9, 2020.

--

--

I am a software engineer who is passionate about frontend development, UX design, machine learning, neural networks, blockchain, robotics and IoT.