johno

GitHub Actions with lerna

Inspired by the automating CI release workflows that next.js and gatsby-mdx implemented, I decided to give it a shot with GitHub Actions for a project I'm working on.

Motivation

Canary deployments/releases have been around for a long time in software. They're a great way to get new features and bugfixes out to a small amount of users right away. This results in a quick feedback loop for motivated users of a product or library.

For busy OSS developers, it comes with another big advantage. Automating canary/ci releases results in less manual publishes allowing you to stick to some type of release cycle while still ensuring new code is available for those who want it. This means a few minutes after you click that green "merge" button a new ci release is live and can be installed with npm i -S library-name@ci.

Without implementing this automated workflow it requires someone on the team to pull down the new code, run the tests, and then run the release script. This then gets even more complicated if you're using npm 2FA and lerna.

So, with a little napkin math, once a PR is merged it takes

  • 15 mins to release (worst case)
  • spread across 10 active projects
  • with a handful of PRs a day

I'm not sure I can even do this math on my napkin, but it sounds like a lot. Especially if you factor in the cost of context shifts and that 15 minutes really means 60 minutes in developer time due to that context shift.

tl;dr it saves substantial time for project maintainers while improving the end experience for library users.

Let's get started

The project in this example is some of my experimentation with Gatsby themes and consists of yarn workspaces and lerna. As a result, the existing Actions workflows weren't a proper fit since I needed a wrapper around yarn and the git binary in the Docker image so that lerna could perform the publish step.

In order to meet these needs I built out a quick action inspired by actions/npm and nuxt/actions-yarn called johno/actions-yarn.

I'll likely write a quick post in the future on how to achieve that.

Setting up a workflow

I like to start building out my workflow with it's entrypoint and the final action. First we define the workflow with its name, and the final action that it resolves, in this case "npm:publish:ci".

NOTE: I prefer to namespace action names with the workflow name, but that's just a personal preference. Any naming convention works.

workflow "npm" {
  on = "push"
  resolves = ["npm:publish:ci"]
}

Then, I define the "npm:publish:ci" action by specifying the action it will use (our custom action above) and the arguments that will be passed to yarn. In this project I've defined a publish:ci script like so:

{
  "scripts": {
    "yarn publish:ci": "lerna publish -y --canary --preid ci --npm-tag=ci --force-publish=*"
  }
}

The action:

action "npm:publish:ci" {
  uses = "johno/actions-yarn@master"
  secrets = []
  args = "publish:ci"
  needs = []
}

Filtering the master branch

The first step we want to make is to filter on the master branch. This means that a commit needs to hit master in order to continue with this workflow.

This is important so that we're only publishing what's been approved and deemed stable.

action "npm:master" {
  uses = "actions/bin/filter@b2bea07"
  args = "branch master"
}

Performing the install

Then we define an action, that depends on npm:master and runs yarn install:

action "npm:publish:ci" {
  uses = "johno/actions-yarn@master"
  secrets = []
  args = "install"
  needs = ["npm:master"]
}

Publishing

Lastly, we finish filling out the npm:publish:ci action by specifying needs = ["npm:install"] to ensure it's performed afterwards.

Creating a bot account for ci releases

I recommend keeping 2FA on your primary npm account and creating a new one (I use johno-bot) and uploading a token for that user to your workflow in the GitHub UI (found when editing your workflow on the site).

This ensures that your main releases are more secure/trusted in the event that your npm account is compromised. Bad actors publishing to a ci tag is substantially lower risk.

Adding secrets

We also add in GITHUB_TOKEN and NPM_AUTH_TOKEN secrets so that lerna can perform the publish with the bot account.

action "npm:publish:ci" {
  uses = "johno/actions-yarn@master"
  secrets = [
    "NPM_AUTH_TOKEN",
    "GITHUB_TOKEN",
  ]
  args = "publish:ci"
  needs = ["npm:install]
}

Conclusion

Automating publishing to a ci tag is a great way to save yourself time and improving the DX for users of your libraries. It's also rad to be able to implement this entirely in GitHub.

Now, I suppose it's time to add tests to this repo so that I can add a testing action to this workflow...

Thanks to the GitHub team for building Actions. Thankts to Tim Neutkens and Chris Biscardi for motivating me to start using this worklow.