GitHub Actions for Monorepos and easy deployments
11 Nov 2023 · 📖 in 7 minutes Making GitHub Actions work for youGitHub Actions are super useful for automating large parts of the repetivie work that goes into building software. I recently wrote a few workflows that I think might be useful for other folks too.
What I'm aiming for
In general it's a good idea to automate repetitive tasks that you find yourself doing often. Most teams will put together the usual Continuous Integrations greatest hits like automatic unit test / linting and automated deployments aren't all that uncommon either.
I recently found myself working in a number of repositories that each had slightly different mechanisms for deploying code. Similarly version numbers were arbitrarily generted for production builds and relied on engineers grabbing a tag, incrementing and pushing to GitHub to trigger a workflow.
Finally, a lot of engineers are using Conventional Commits now and this unlocks interesting possibilities for versioning and release notes.
Automating prod deployments and release notes for NodeJS
Production deployments mean different things to different teams and pipelines are going to differ between technologies. For most web apps these days, React, Vue, Angular etc there's a good chance a package.json
file is being used to organise the project's dependencies and related config.
Building npm
packages has always been super easy (just a quick glance into your node_modules
will confirm as much) and that means npm
comes with a bunch of useful versioning commands. The workflow below uses npm version
along with mathieudutour/github-tag-action to take a tag, generated from the semver interpretation of the conventional commit messages.
Versions from conventional commits
Take the following commits:
- chore: Fixing homepage css
- feat: adding new login prompt
- revert!: implementing edit button
Here is an example of the 3 different levels of semver that can be achieved, with the above commits each indicating a
patch
,minor
, andmajor
.When combined we take the most significant semver (i.e
major
.minor
.patch
) which for the above example would be a bump on themajor
.While semver is useful for libraries in indicating breaking changes between versions, it's also a useful mechanism to remove the manual steps of bumping version numbers in apps.
The bulk of the heavy lifting is done by the github-tag-action
which calculates the magic semver for us. Normally we could push the tag there and then but we want to leave that to the npm version
step just below. The other nice thing it does is generate a changelog
which collates all the different commit messages into one long string. The changelog will be useful for when we want to generate the release notes.
As mentioned npm version
manages our version and in the action it's creating the new tag, updating the package.json
and creating a commit which leaves a simple git push
to sync the release changes (note that the npm version
will create the tag as part of it's invocation so the user.name
/ user.email
needs to be set before).
Once the tags / package bumps have been synchronised we can then generate release notes. Once again the heavy lifting is done by ncipollo/release-action@v1 and takes the changelog
produced earlier and creates a release note that is helpfully grouped according to the conventional commits found between the current and previous tags.
You can check out the workflow below:
name: Create Prod Release
on:
workflow_dispatch:
# TODO maybe we don't need all
permissions: write-all
jobs:
tag_version:
runs-on: ubuntu-latest
outputs:
new_tag: ${{ steps_tag_version.outputs.new_tag }}
steps:
- uses: actions/checkout@v2
- name: Generate tag
id: tag_version
uses: mathieudutour/github-tag-action@6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
dry_run: true
- name: Update package.json and push
run: |
git config user.name "${{ github.actor }} (Automated)"
git config user.email "github-actions@github.com"
npm version ${{ steps.tag_version.outputs.new_tag }}
git push
- name: Create Release Notes
uses: ncipollo/release-action@v1
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: Release ${{ steps.tag_version.outputs.new_tag }}
body: ${e steps.tag_version.outputs.changelog }}
# optional deploy step
#deploy:
# needs: tag_version
# uses: ./.github/workflows/deploy-workflow.yaml
# with:
# tag: ${{ needs.tag_version.outputs.new_tag }}
I left parts commented out on purpose as the workflow by itself is more than useful however if you wanted further steps to be executed, like the deployment, then you can see how that might be done.
Give it a go and let me know if you find it useful!
Simplified deployments in a monorepo
Next up is a useful workflow for monorepos. Love them or hate them there's certainly some benefits to small to medium size monorepos when you're working on related projects and especially more so given the "gitops" style of deployments that are common.
Note
Huge, company-wide monorepos that you'd find at Google, Meta and the like are a different beast entirely and almost certinaly rely on custom tooling (and likely use modified versions of git). For these companies it makes a lot of sense to standardise across the business preventing repeated work and highlighting dependencies, to name a couple.
However there are still benefits to creating monorepos for smaller or personal projects. My blog is currently in a monorepo with the "builder" as I'm actively working on it and being able to keep everything in sync is valuable to me. There's also a bunch of tooling around if you're interested, things like Lerna and Turborepo are great for JS / TS repos.
The below workfow relies on the workflow_dispatch
approach which requires a manual trigger over in the GitHub actions page for a given repo. This is actually pretty great for a monorepo as a tagged commit workflow would require some namespacing on order to trigger actions for a specific part of the repo.
The workflow is similar to that above using the mathieudutour/github-tag-action
to generate tags for the change. The interesting part is the environment selection which allows the actions to build for prod
and dev
by simply tweaking the input option. This is further useful as it can be used to selectivley trigger creation of the ncipollo/release-action
.
name: Create Release
on:
workflow_dispatch:
inputs:
service:
description: "Select a service"
required: true
type: choice
options:
- frontend
- service_one
- service_two
- database
environment:
description: "Select target environment"
required: true
type: choice
options:
- dev
- staging
- prod
permissions: write-all
jobs:
tag_branch:
runs_on: ubuntu-latest
outputs:
new_tag: ${{ steps_tag_version.outputs.new_tag }}
steps:
- uses: actions/checkout@v2
- name: Bump version and push tag
id: tag_version
uses: mathieudutour/github-tag-action@6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag_prefix: ${{ inputs.service }}-${{ inputs.environment }}-v
- name: Create Release Notes
if: ${{ inputs.environment == 'prod' }}
uses: ncipollo/release-action@v1
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: Release ${{ steps.tag_version.outputs.new_tag }}
body: ${e steps.tag_version.outputs.changelog }}
deploy_frontend:
needs: tag_branch
if: ${{ inputs.service == 'frontend' }}
uses: "./github/workflows/frontend-deploy.yaml"
with:
tag: ${{ needs.tag_branch.outputs.new_tag }}
secrets: inherit
deploy_service_one:
needs: tag_branch
if: ${{ inputs.service == 'service_one' }}
...
deploy_service_two:
needs: tag_branch
if: ${{ inputs.service == 'service_two' }}
...
Once again the bulk of the action is implemented and the follow up deploy_....
steps are left as an example for the reader.
Lemme know what you think.
GitHub Actions are cool, mostly
I like the actions and they immediately pay dividends, however working with actions can be a real pain and there's many iterations needed to get to this point and that's partly the reason I'm writing this post.
Good luck and share you favourite actions with me over on Mastodon.
First appeared on Trusty Interior, last update 2 Nov 2024