Using Semantic Release and Github Actions to build releases

This post describes how you can use both semantic-release and Github Actions to package and attach to a release a new version of your product — automatically and based on your commit messages. For the purpose of this demo, we are packaging a very simple zip file with a single txt file inside, but hopefully you get the idea.

These are the concepts I will demonstrate in the demo:

  1. Running your tests in CI
  2. Determining whether CI needs to package a new release
  3. Setting up the correct repo permissions
  4. Using a build script to create your new package using version and release notes from semantic-release
  5. Generating a new Release with your package attached
  6. Committing any changed files during packaging back into the repo

I've also create a demo project for the purpose of this post: https://github.com/whomwah/demo-semantic-release-gh-actions

During the post we'll take a look at the various files in this repo and talk about what they do, and how you could change them to suit your project.

Running your tests in CI

This first concept is something you probably do on all your projects (you have tests right?) and that is run your tests suite. In the context of semantic-release it doesn't matter what tech you are using here. Just because semantic-release is a JavaScript project, it doesn't mean you're limited to JavaScript projects.

So in the case of this demo we just have a very simple mod.ts file and a supporting mod.test.ts which we want our CI to run and pass before we build anything. We also a have a .github/workflows/tests.yml action file. This will simply run our tests for this project in Github. We are using Deno to run our tests as it's not only a great tool, but is a very easy way to run specs on TypeScript files without having to pull in a build system.

Determining whether CI needs to package a new release

Once your tests have run, we then need to know whether we should start a new release. If we're just running the tests in a PR branch for example we don't. This is where our .github/workflows/package.yml file kicks in.


...
on:
  workflow_run:
    workflows: [Tests]
    types:
      - completed

  release:
    if: ${{ github.ref_name == 'main' && github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    steps:
...

Firstly you can see the workflow_run section. Here we are referencing our Tests workflow which runs the tests. This is basically saying "run this workflow once the Tests workflow has completed". The next part is making sure that we only run the steps in the workflow if we are currently on the main branch and also that the conclusion of the previous workflow was a success, ie: the tests passed.

Setting up the correct repo permissions

Before we get any further into the package workflow, we need to first make sure our repo has the correct permission in order to be able to run the workflow completely. At the start of each workflow run, GitHub automatically creates a unique GITHUB_TOKEN secret to use in your workflow. You can use the GITHUB_TOKEN to authenticate in a workflow run. This is great but unfortunately the token does not have enough permissions to be able to create a new commit, which we will need to do in our demo because we are changing the VERSION file in the root. We want to commit this updated file as part of the release process. In order have enough permission for this we need to create a new ENV to use.

Firstly we visit https://github.com/settings/tokens and create a new access token. When creating the token we need to make sure that all the scopes in the repo section are ticked. Once we have created the token, make sure it is visible to copy as you will need it for the next bit. Now visit https://github.com/<your_repo>/settings/secrets/actions where you will create a new secret. We'll call this secret GH_AUTH_TOKEN and the value will be the access token you created before that. The workflow will now be able to access this value via secrets.GH_AUTH_TOKEN.

Using a build script to create your new package

In this demo, we are using a build script to create our final release asset. We do a couple of interesting things for the purpose of the demo which you will find in the comments of the build file. The important thing is though that we are passing the build script the version of the next deployable version and also any release notes that were gleaned from the commits that triggered that new release.

Generating a new Release with your package attached

Now we have created our new release package, we want to trigger semantic-release to do it's stuff. Let's go back to our .github/workflows/package.yml file.


...
steps:
  - name: Checkout
    uses: actions/checkout@v3
    with:
      fetch-depth: 0
      token: ${{ secrets.GH_AUTH_TOKEN }}

  - name: Setup Node.js
    uses: actions/setup-node@v3
    with:
      node-version: 16
...

This first section installs the action dependencies we need. Ie: We need to install NodeJS in order to be able to install semantic-release and run it.

The next section does all the magic.


...
  - name: Setup package.json
    run: echo '{"name":"demo", "devDependencies":{"@semantic-release/git":"^10.0.1","@semantic-release/exec":"^6.0.3","semantic-release":"^19.0.5"}}' > package.json

  - name: Install dependencies
    run: npm install

  - name: Release
    run: npx semantic-release
    env:
      GH_TOKEN: ${{ secrets.GH_AUTH_TOKEN }}
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
...

Firstly, we generate a package.json file inline for the project. It contains all the dependencies we need in order to run semantic-release for this demo. We use @semantic-release/git and @semantic-release/exec to both commit our changes back to our repo and also to run our build script. You can then see that npm install is then run which will install all these dependencies. The final step than actually runs semantic-release which will generate the new release for us if required by our commit messages.

So let's go through the .releaserc file and explain what's going on. It's this file that will be picked up when npx semantic-release is called.

This first section basically says only run this if we are on the main branch. We do this check in our Github Action too so this is just a double check.


{
  "branches": [
    "main"
  ],
  ....
}

This next section uses the built in commit-analyzer and release-notes-generator plugins which are the ones that check your commit messages and see if any of them mean a new release should be created. The final section that calls @semantic-release/exec is the important one. It's this command that calls our build script and passed it the new version we're building and any release notes we have.


{
  ...
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    [
      "@semantic-release/exec",
      {
        "prepareCmd": "./bin/build_release ${nextRelease.version} \"${nextRelease.notes}\""
      }
    ],
  ]
  ...
}

Next we attach our built package to the new release we are creating by pointing the file/s we want to attach. See we can use the nextRelease.version to argument the text.


...
[
  "@semantic-release/github",
  {
    "assets": [
      {
        "path": "releases/*.zip",
        "label": "Our Demo Package (${nextRelease.version})"
      }
    ]
  }
],
...

Committing any changed files during packaging back into the repo

During our build step, you know that we made a change the to the local VERSION file in our repo? That change was simply to show how you can make change to files in your repo as part of the package workflow. This final step in process will actually commit that change back into the repo.


...
  [
    "@semantic-release/git",
    {
      "assets": [
        "VERSION"
      ],
      "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
    }
  ]
...

You can see an example of that commit in the demo project https://github.com/whomwah/demo-semantic-release-gh-actions/commit/19bde86d89c871ef4e08d8f882ba757d84f174aa.

Finished

So that's a run through of building and packaging new release assets. If you look in the releases section for the demo repo you can see all the releases created and how the zip file we packaged up is attached.

I hope you find this useful. Here are some useful links to support the post:

 

We are Kyan, a technology agency powered by people.

 


 

Previously from Duncan:

Using Swift/SwiftUI to build a modern macOS Menu Bar app
A curious case of the QR code, Christmas Day, and 20 million downloads
Building our own office Jukebox using Mopidy, NodeJS and React