In this post we'll look at setting up a Monorepo and integrating it with GitHub Actions. Nx will be used as our monorepo tool. We'll use a GitHub Action to deploy an application to Firebase and another Github Action to deploy an API to Firebase.
Our build pipeline will be split into frontend and backend sections. Changing only APIs will only trigger the backend actions and changing the app code will only trigger the frontend actions. Changing both will trigger frontend and backend actions.
A link to the github repository accompanying this post can be found here and in the resources.
First lets address what and why Monorepos exist.
A monorepo is a single repository containing multiple distinct projects, with well-defined relationships.
So if you are working in a team that has multiple distinct projects that are related a monorepo is a good choice, it will make standardizing tooling simpler and reduce inefficiencies related to dealing with multiple repositories.
So why would we want to integrate with GitHub Actions.
GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub. Make code reviews, branch management, and issue triaging work the way you want.
Creating Our Monorepo
For the purposes of this post we'll use Nx as our monorepo tool. Nx can be used for frontend and backend projects. It's plugin system allows developers to choose various tech stacks to work with. We'll use Angular and Nodejs (Nest will be used as Server Side framework).
Creating our monorepo can be done by running the below command.
npx create-nx-workspace@latest monorepo-github-actions
I've opted for the angular-nest preset which will setup a fullstack application using Angular for frontend and Nest for the backend.
You can skip the preset options with below command, if you like.
npx create-nx-workspace@latest monorepo-github-actions --preset=angular-nest
You'll be prompted to give you application a name and to choose your default stylesheet format.
You can also choose to use Nx Cloud at this point. I would recommend doing so at this point so you can evaluate it and determine if it's going to work for your project.
Now to set up Firebase. First you need to install the Firebase CLI.
Once Firebase is installed, you can start installing dependencies and initializing the Firebase project.
cd monorepo-github-actions && npm i firebase-functions express @nestjs/platform-express
At this point we'll select only the Functions option. We could also select Hosting but that resulted in an error with my version of firebase CLI (11.4.2).
Once complete you will have a firebase.json and a .firebaserc file created. We'll modify these shortly.
firebase init hosting
Update package.json with below:
A functions directory was created but can now be deleted, as we will update the firebase.json file to point to the current directory instead.
Note that we are saying we want Firebase to deploy our frontend app, which once built will be located in dist/apps/monorepo-github-actions. You will need to ensure the name matches your app name.
Modifying Nest Server
In order to make our API work with Nest we need to modify the bootstrapping code in apps/api/src/main.ts. It should look like below:
To verify Nest is working as expected try running it locally:
nx serve api
To verify you can run the app try running it locally:
nx serve monorepo-github-actions
Adding a GitHub Action
A GitHub Action can be added to a project by creating a .github/workflows directory and adding a <my-workflow>.yml file that describes what action you want to trigger and when it should be triggered, e.g. when a push or pull request is performed.
GitHub actions offer a lot of flexibility to ensure you can execute the workflow that works for your particular projects. You can run shell scripts, you can define what branches they should be triggered on and you can integrate community created actions that solve common issues, e.g. how to filter on paths and only execute workflows on particular directories (https://github.com/dorny/paths-filter). This is exactly what we need when working on a monorepo.
We want to trigger builds only if certain paths within our monorepo have changed. For the purposes of this post we'll separate our builds into frontend and backend.
The workflow below will trigger an action on our API or our application or both, if there is a change committed and pushed to either. In this way we can ensure actions are only triggered if there is a change to the app or api in our Nx monorepo.
If there are changes to the frontend the action will deploy the changes to Firebase so any reviewers can see the change live. If the change is to the backend API it will deploy the updated API where reviewers can test the functionality.
Now we have all the pieces necessary but we don't yet have a github repository for our monorepo. So create a Github repository and push this code to it to get started. If you want you can fork the sample repository that accompanies this blog post, it's linked below.
When you push updates to your repository you should see the actions being triggered as shown below.
Verifying Deployed API
Once you push a change to your API you should see a Function URL listed under the backend section on the Actions tab for your Github repository. It will be under the Deploy to Firebase Fuction section. By default this URL will give a 403 Forbidden response as security is enabled by default.
There are two options to access you deployed API:
- keep it secure and require a token for access
- make your API publically available
Lets start with option 1, which is less secure but may be what you need depending on you API. You should be aware that you may incur costs if your API starts getting used a lot and factor this into your development planning.
To enable access for all users you need to navigate the the Google Cloud console and add permissions for your API:
- Select your project in toolbar if you have more than one
- Select the function you want to enable access on
- Click Permissions
- Click Add Principal
- Type allUsers in New Principals input
- Select role > Cloud Functions > Cloud Functions Invoker
Configured permission is shown below. Now if you navigate to your deployed app you should see the welcome message returned from your API.
Option 2, is a bit more involved but is a better choice when you are starting out and if you need authentication. Below covers how to successfully call you API using curl.
To get a successful response we need to call the API with a valid Bearer token. To do this we'll need to install gclould. See https://cloud.google.com/sdk/docs/install.
Once gcloud is installed we can call our API using gclould to get the token:
curl -m 70 -X GET <function-url-here>/hello -H "Authorization:bearer $(gcloud auth print-identity-token)"