Automated versioning and package publishing using GitHub Actions and semantic-release
Tags: javascript, github, webdev, devops
Published: 2021-08-01 11:39:27

When we are developing JavaScript packages, there is a series of repetitive tasks that we have to complete manually every time we have to publish a new release to npm:

- Change the version field in `package.json`
- Create a new Git tag and a GitHub release
- Execute any build steps to create the release artifacts
- Update the changelog
- Publish to npm

Wouldn't be great if we could automate all of these tasks?
**[GitHub Actions](https://www.github.com)** and **[semantic-release](https://github.com/semantic-release/semantic-release)** have us covered!


**GitHub Actions** is a GitHub feature that lets us build, test, and deploy our GitHub hosted projects. You can think of it as the CI/CD pipeline for GitHub. It uses YAML files, called `workflows`, that trigger based on specific events (e.g. when a commit is pushed).

**semantic-release** is a tool that uses the [Conventional Commits](https://www.conventionalcommits.org) message format to determine the type of changes in our code base. It automatically sets the next [semantic version](https://semver.org/) number, generates the changelog and publishes the release.

Let's start by preparing our repository.

## Check existing version tags
If we are going to use **semantic-release** in an **existing repository** we'll first have to make sure that the most recent commit included in the last published npm release is in the release branches history and is tagged with the version released.

> You can skip this step if you are setting up **semantic-release** in a new repository.

Assuming our release branch is `main`, last commit `SHA` is `1234567` and current published version of our project is `v1.1.0`

```bash
# Make sure the commit 1234567 is in the release branch history
$ git branch --contains 1234567

# If the commit is not in the branch history 
# we need to configure our repository to have the last release 
# commit in the history of the release branch

# List the tags for the commit 1234567
$ git tag --contains 1234567

# If v1.1.0 is not in the list we have to add it with
$ git tag v1.1.0 1234567
$ git push origin v1.1.0
```

## Remove version from package.json
Since **semantic-release** takes care of updating the package.json’s version before publishing to npm, we can set `"version": "0.0.0-semantic-release"` inside our `package.json`.

## Create an npm token
In order for our GitHub action to be able to publish our package to npm, we're going to need an [npm authentication token](https://docs.npmjs.com/creating-and-viewing-access-tokens).
Login into your [npm](https://www.npmjs.com/) account, click on the profile icon, and select **Access Tokens**. Then click on **Generate New Token** -> **Classic Token**, fill in a name for your token e.g. `my_token`, select the **Automation** token from the list and click **Generate Token**. Copy the token and make sure you don't lose it, as we're going to need it for the next step.

## Add the npm token to the GitHub's repository secrets
Navigate to your GitHub repository page, click **Settings** and then **Secrets and variables** -> **Actions**. Click on the  **New repository secret**, fill in `NPM_TOKEN` as the **Name**, paste the npm token created on the previous step inside the **Value** field and hit **Add secret**. 

Next, on the left menu, navigate to **Actions** -> **General** scroll down, and in **Workflow permissions** select **Read and write permissions**.

That's it, now the `NPM_TOKEN` can be used as an environment variable inside our GitHub release action.

## Create the GitHub release action
Let's create the GitHub release action that will run every time we push a commit to our `main` and `beta` branches. The `beta` branch will be used for our [pre-releases](https://semantic-release.gitbook.io/semantic-release/usage/workflow-configuration#pre-release-branches) in case we need any.

Create a `.github/workflows/release.yml` file in the project's root with the following contents.

`.github/workflows/release.yml`
```yml
name: Release

on:
  push:
    branches: [main, beta]

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v2
    - name: Setup Node.js
      uses: actions/setup-node@v1
      with:
        node-version: 20.x
    - name: Install dependencies
      run: npx ci
    - name: Install semantic-release extra plugins
      run: npm install --save-dev @semantic-release/changelog @semantic-release/git      
    - name: Lint
      run: npm run lint-fix
    - name: Test
      run: npm run test:unit --if-present
    - name: Build
      run: npm run build      
    - name: Release
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      run: npx semantic-release
```

Here we're using Node.js version 20, as it's a requirement of `semantic-release`, so make sure you align that with your project's node version.
We also have steps for linting, testing, and building our code. Go ahead and remove or change these as you see fit.

The important parts are the `Install semantic-release extra plugins` and the `Release` steps.

> Notice that we don't have to install **semantic-release** as it comes pre-installed in GitHub actions by default. We just need the `@semantic-release/changelog` and `@semantic-release/git` plugins.

Inside the `Release` action you'll notice two environment variables
- `GITHUB_TOKEN`
That is the token used to authenticate to GitHub. This is an automatically created secret to use in our workflow and it is needed by **semantic-release** in order to be able to create Git tags.

- `NPM_TOKEN`
Is the npm authentication token that we created and added to our repository previously. We'll need this in order for our action to be able to publish our package to npm.

## semantic-release configuration
**semantic-release** configuration can be set by using a `.releaserc` file, a `release` key inside `package.json` or a `release.config.js` file in the project's root. We'll use the latter.

`release.config.js`
```js
module.exports = {
  branches: [
    'main',
    {
      name: 'beta',
      prerelease: true
    }
  ],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    [
      '@semantic-release/changelog',
      {
        changelogFile: 'CHANGELOG.md'
      }
    ],
    '@semantic-release/npm',
    '@semantic-release/github',
    [
      '@semantic-release/git',
      {
        assets: ['CHANGELOG.md', 'dist/**'],
        message: 'chore(release): set `package.json` to ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
      }
    ]
  ]
}
```

The `branches` attribute includes the branches on which releases should take place. Apart from `main` we also include a `beta` branch with `prerelease: true`, this way we can have [beta versions](https://semantic-release.gitbook.io/semantic-release/usage/workflow-configuration#pre-release-branches) published using a `beta` branch.

In the `plugins` section we define the list of **[semantic-release plugins](https://semantic-release.gitbook.io/semantic-release/usage/plugins)** to use. The plugins we have defined are already part of **semantic-release** so we don't have to install them separately.

- `@semantic-release/commit-analyzer`
It determines the type of our release (e.g. `major`, `minor`, `patch`) by analyzing commits with [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog). **semantic-release** uses [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) by default.

- `@semantic-release/release-notes-generator` 
It generates the release notes for the changelog.

- `@semantic-release/changelog`
It creates and updates the changelog file, with the content created by the `release-notes-generator` in the previous step.

- `@semantic-release/npm`
It publishes the npm package

- `@semantic-release/github`
It publishes the GitHub release and comment.

- `@semantic-release/git`
It commits the release artifacts to the project's Git repository. In this example we're committing the changelog file and all files inside the `dist` folder. We're also defining the message for the release commit. 
> Note: `[skip ci]` in the commit message is used in order to not trigger a new build.


## Enforce conventional commits with commitlint and husky
Since `semantic-release` uses the conventional commits format to automate the versioning, we need to make sure all commits in our repository follow the appropriate format. 

For this purpose we are going to use **[commitlint](https://commitlint.js.org)** and **[husky](https://typicode.github.io)**.
We'll leverage **husky** to add a Git hook that uses **commitlint** to check whether our commit message meets the conventional commit format, every time we commit.

### Install commitlint
```bash
npm install -D @commitlint/cli @commitlint/config-conventional
```

add the commitlint config file into the project's root
`commitlint.config.js`
```js
module.exports = {
  extends: ['@commitlint/config-conventional']
}
```

### Install husky
```bash
npm install -D husky
```

Enable husky
```bash
npx husky init
```

The `npx husky init` command sets up husky in the project by creating a `pre-commit` script in `.husky/` and inserting/updating the `prepare` script in `package.json`.

Add a hook to lint commits using **commitlint** before they are created, using **husky**'s `commit-msg` hook:
```bash
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
```

## Ready to publish
We've finished the setup and configuration of **semantic-release** in our **GitHub** repository. From now on we have to use the Conventional Commits specification for our commit messages. 

For example, if our package is now at version 1.0.0, a commit message with this format:

`fix(homepage): fixed image gallery` will bump the version to 1.0.1

`feat(logging): added logs for failed signups` will bump the version to 1.1.0

**That's all there's to it!**

**semantic-release** and **GitHub Actions** will take care of the rest, determining the next version number, generating the release notes and publishing the package to npm.