CICD for frontend with Firebase and GitHub Actions
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 fewyaml
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.
- Create a React-based frontend app
- Use Firebase todeployour app
- 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 necessarypermissions.
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 tohttps://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
Here, the only
flag specifies that the app should be deployed only to the site mapped to the target name 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 Valuetextarea and click on Add secret. I used FIREBASE_TOKEN
as the name.
Next, we need to create a GitHub personal access token. When we bump the version of our app during CICD, the package.json
file and the package-lock.json
files need to be committed and pushed to the repo. Since, as mentioned earlier, the deployment process will be running on containers, the container should be granted access to our repo through our personal access token.
Create a Personal Access Token
To create a personal access token, go tohttps://github.com/settings/profileand 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.
GitHub provides us with a boilerplate to help us bootstrap Actions easily. We only need to modify a few things to get it to do what we want it to. We shall be using this workflow to set up staging. So, let’s name this file staging.yml
. Let’s also set the name
attribute to “Staging”.
The on
attribute specifies what should trigger the action. Under the on
attribute, you can specify the event that should trigger it. Under the event, you need to specify the branch that should trigger it. Since we want this to run when both a pull request is raised and merged to the master branch, let’s leave the setting as it is.
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
We can use the steps
attribute to specify the steps. We can create steps by prepending the first line of the step with a hyphen. The lines that follow it will belong to that step. The uses
attribute is used to run an action. An action is a predefined step that is available in the marketplace.
In the boilerplate, you will see checkout@v2
and setup-node@v1
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.
- Build the app
- Deploy to Firebase
- Bump the version
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 canauthenticateourselves using the personal access token.
The first step in our configuration checkouts our repo using the checkout@v2
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 conditionhereas well.
- name: Version Bumping run: | git config --global user.email "[email protected]" 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.
To prevent this, we need to make sure the action is not triggered when the version is bumped. Fortunately, GitHub Actions allows us to ignore certain paths when triggering actions. When we bump versions, only the package.json
and package-lock.json
files get pushed. If we can ignore these two files, then this action won’t be triggered.
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 checkout@v2
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 Actionshere.
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.
Leave a Reply