David Hockley

GitHub Actions + release-it: How to Release Your Code Into the Wild

Coding is all very well. But inevitably, there comes a time when we need to compile the code we've produced and push the end result out into the wild. Be it in the form of an app, a website, a program, a shared library, or an NPM module…

And that might start out simple to manage.

But as your project grows, so does the complexity of pushing out your code. The more moving parts there are, the more there are opportunities for something to break.

For example, we use a PHP backend stack that integrates React apps at work. So pushing a new build into production includes :

  • downloading any translations from the translation tool we use
  • running a linter to check code quality,
  • running unit tests,
  • minifying javascript code
  • updating the version number
  • **building the various React components **into production bundles,
  • pushing the compiled javascript to the CDN
  • pushing the PHP code to the server on AWS.

Forgetting any of those steps is a recipe for trouble. I've wasted hours trying to remember what to do, and how to solve the issue troubling the deployment process. It's even worse when you're trying to remember how to update a project you haven't worked on for 3 months.

So what is the answer?

In principle, the solution is simple: automate everything that can be.

And I have good news for you: there is a powerful tool that allows you to automate a lot of things. It's called GitHub Actions. But it is daunting, so today we are going to look at:

  • how to use release-it and auto-changelog to script your release process
  • how to set up GitHub actions to automate the release process

Releasing an NPM module using release-it

First, let's look at how to deploy our code. I'm going to be working on a simplified version, using an NPM module(https://github.com/Kodaps/faker) I created.

A word of caution here before going any further. Releasing NPM modules with GitHub Actions is a lot more complicated if you are using yarn instead of npm. My recommendation is to run git rmon any yarn.lockfiles you might have and only use npm. (It is feasible to use yarn, but it's quite a bit more complicated).

Now, there are five steps I use every time I update my npm package.

  1. I make sure my code is up to date (with git pull).
  2. I ensure that the code is clean by running unit tests (with Jest ) and linting (with ESLint).
  3. I prepare a changelog that lists all the changes since the last version.
  4. Then, I set the new version number.
  5. Finally, I commit and publish the whole thing, both as a release to GitHub and as a package to NPM.

A quick tip here: Whenever I automate anything, I start out by creating a local script file that runs all the steps sequentially. So for example, for pushing out code, I tend to create a batch file called "up.sh" that sits in my project's root and runs all the tasks.

But I've come across a tool that covers all my publication needs and then some. It is an NPM module called release-it(https://github.com/release-it/release-it), and I use it in pretty much all my projects now. Allow me to show you how it works.

First, to set it up, we run :

   npm init release-it

The tool asks a few questions. It either stores its configuration in the package.json or a separate file called .release-it.json. I personally prefer to go the second way, because my package.json files usually already have quite a lot of stuff in them.

Now, of the five steps I mentioned, one was tracking and updating the version number. This already comes baked into the release-it tool. One step is done, four to go.

By default, release-it will also refuse to do anything if the git repository is not in a clean state. For example, if there is uncommitted work in progress. So that's already taken care of by default.

The next thing is to make sure we're only releasing from the git branch that is actually meant to be released. For that we add a constraint in the configuration file ( .release-it.json ) like so :

    {
      "git": {
        "requireBranch": "main",
      }
    }

This is pretty self-explanatory: only run if you are on the "main" branch. At work, we use a branch called "preproduction" to stage and test our code, so I use that instead.

While we're here we can also specify the commit message that will be created releasing the package :

    {
      "git": {
        "requireBranch": "main",
        "commitMessage": "chore: release v${version}",
      }
    }

Now, the next steps are to make sure my code is up to date, and test it. I've already set up tests in my project using Jest and EsLint, and I run them using "npm run test" and "npm run lint".

I want these to run at the start before anything happens, so I use a release-it lifecycle hook, and there a quite a few. And so in my configuration file for release it, I now have a list of three items in the before:init hook:

    {
      "git": {
        "requireBranch": "main",   
        "commitMessage": "chore: release v${version}",
      },
      "hooks": {
        "before:init": ["git pull", "npm run lint", "npm run test"],
      }
    }

This is all also fairly self-explanatory. We've defined a hook for release-it that runs before it gets to work, and this updates the code with git pull. Then it checks for linting errors with npm run lint and runs unit tests with npm run test.

A final thing I like to do is to add a changelog; to list everything we've done between releases. But the commits we've written already state what we've done, so we can automate this too.

And to do so, we'll be using a tool called auto-changelog(https://github.com/cookpete/auto-changelog).

To trigger it we are going to use another hook called after:bump. As its name implied, it runs once the version number is "bumped" up.

    {
      "git": {
        "requireBranch": "main",
      },
      "hooks": {
        "before:init": ["git pull", "npm run lint", "npm run test"],
        "after:bump": "npx auto-changelog -p",
      }
    }

Here we add a -p flag to tell the tool that the version number is stored in the package.json file.

This will create a changelog file using the commit data, and create the release info.

Finally, we specify that we want to release the code to GitHub and publish it to NPM.

  {
      "git": {
        "requireBranch": "main",
      },
      "hooks": {
        "before:init": ["git pull", "npm run lint", "npm run test"],
    	"after:bump": "npx auto-changelog -p",
      },
      "github": {
        "release": true
      },
      "npm": {
        "publish": true
      }
    }

Now is the time to test it out. If you look at your package.json file, the init should have added a release entry in the scripts section :

    {
      [...]
      "scripts": {
        "release": "release-it",
        [...]
      },
    }

This allows us to run npm run releaseto trigger the release process. When we do so, we get asked a series of questions: do we want to define this as a minor upgrade, a major upgrade, or a breaking change? Do we want to publish? And so on. This allows us to run through the process and check what is going on.

And once that is working the way you want it, now is the time to … automate it even more!

Using GitHub Actions to automate things even more

Understanding the workflow file

For that we are going to use GitHub actions. Now, what are GitHub actions and how do they work ?

Imagine you're in a company and a new developper joins your team. What do you do?

Well, first you buy him a new computer, then you help him download the code, install everything and get to work.

In a sense, GitHub Actions does the same thing. It sets up a container with an operating system. Then it downloads software and a current version of our code. And finally, it carries out a set of tasks…

To do this we create a folder called .github. In that folder, we create another folder called workflows. And in this folder, we create a file called release.yml.

There are three required fields here: name, on, and jobs. These define what the workflow is called, when it is triggered, and what actions it takes.

In our case, the workflow is going to be called "Release & Publish to NPM" so we fill in the name correspondingly.

And it will be triggered manually, so we set the "on" field to: "workflow_dispatch". (We can add several different triggers here, and configure them, but in our case, we don't need to).

Our release configuration file now looks like this :

    name: Release & Publish to NPM
    on: workflow_dispatch

Now to the fun part, the jobsfield! This defines a list of jobs that can be run, and each job is a list of steps. Our first (and only) job is going to be called release, and now we get to configure it!

We start by specifying which operating system we want to run using the "runs-on" parameter. Here I've specified ubuntu. And now we define the list of steps. We'll start simple here, just to test things out

The first thing we do is retrieve the code, and for that, we will use a prepackaged Action called "actions/checkout@v2". This one is created by GitHub, and there are loads of available actions for loads of different use cases.

In the second step, we install the dependencies, and for that, we simply specify in the step the command we would like to run. So here we have the action execute npm ci (which runs a clean install).

Finally, we log that the job is done, and in the same way we simply run an "echo" command.

  jobs:
      release:
        runs-on: ubuntu-20.04
        steps:
          - name: Checkout source code
            uses: actions/checkout@v2
          - name: Install the dependancies
            run: npm ci
          - name: End message
            run: echo 'All done!'

`Let's start by testing this out. To do so, we commit the workflow file and push it to the main branch.

As you can see when you head to the repository, there is an "Actions" tab. Our workflow is now listed there. Here, we can trigger the workflow manually. To do so we click on the "run workflow" button in the user interface.

When it has finished running (or even while it is still running!), we can open up the logs. We can see where the different steps are at, and if there are any errors.

#### Configuring git and NPM

The next step is to finish setting up git and npm in the workflow. To do so we add another step in the list, after the checkout action, and we state the git user and email we want to use
```yaml
  - name: Initialize Git user
          run: |
              git config --global user.email "david@kodaps.com"
              git config --global user.name "Release Workflow"

Now, we'd need to be able to publish to NPM without logging in, and for that, we need a "publish" token.

To retrieve this, we need to head to the NPM website. Once logged in, we need to select Access Tokens. Then back on GitHub, on the repository page, we click on Settings and then Secrets > Actions. Here we click on "New repository secret" and enter NPM_TOKENas the secrets name, and paste in the value of the token.

Now we need to configure npm to use the token when talking to the NPM registry. We use the npm config setcommand, and we pass in the secret NPM_TOKENthat we've just generated/

    - name: Initialise the NPM config
            run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
            env:
              NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Now, there is a problem. The command creates a .npmrcfile on the container. And that file will block the release because the git directory will no longer be clean.

So let's go and add that file to the .gitignore file:

    #.gitignore
    node_modules/
    .env
    .npmrc

And now for the release

Now, let's replace the final log with the command to publish the package, using the release command and the --ciflag (which stands for continuous integration).

We also need to provide two tokens to the action. The first is the GitHub token, and this is provided by default by the action.

- name: Run release
  run: npm run release --ci
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Our final workflow now looks something like this


##.github/workflows/release.yml
    name: Release & Publish to NPM
    on: workflow_dispatch
    jobs:
      release:
        runs-on: ubuntu-20.04
        steps:
        - name: Checkout source code
          uses: actions/checkout@v2
        - name: Install the dependancies
          run: npm ci
        - name: Initialise the NPM config
          run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
          env:
            NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        - name: Initialize Git user
          run: |
            git config --global user.email "david@kodaps.com"
            git config --global user.name "Release Workflow"
        - name: Log git status
          run: git status
        - name: Run release
          run: npm run release --ci
          env:
            NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

In closing

I find the combination of release-it and GitHub Actions to be powerful. Together they ease a recurring pain point of mine, be it in personal projects or at work.

So far I've only applied it to NPM modules and NextJS websites (although I also use GitHub actions to automatically update my profile(https://levelup.gitconnected.com/how-to-easily-automate-your-github-profile-to-showcase-your-work-126edab12d3c)), but I'm looking forwards to also using them to build and release mobile apps. And of course, you can do lots of fun stuff like sending Slack or Discord notifications, setting up a cron to trigger API calls, and so on. Let me know if you'd like me to explain how!

Social
Made by kodaps · All rights reserved.
© 2023