Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save egdelgadillo/2294da4d387765c0ae993c27787a3491 to your computer and use it in GitHub Desktop.
Save egdelgadillo/2294da4d387765c0ae993c27787a3491 to your computer and use it in GitHub Desktop.
Setup versioning and NPM publication using Github Actions and changesets

Setup versioning and NPM publication using Github Actions and changesets

In order to automate semantic versioning, GitHub releases and NPM package publishing we can leverage Github Actions alongside Changesets Release Action.

For this guide, I'll be following the steps I used in my require-ts-check ESLint Pluggin repo.

Table of contents

Setting up your repository permissions

First and foremost we need to create a new repository if we don't have one already. After this is done, next step is to allow Github Actions to create Pull Requests.

For this step, while in our repository, we need to navigate to SettingsActionsGeneral. Scroll down to the bottom where you'll see Workflow permissions. In this section we need to:

  • Grant read and write permissions to GITHUB_TOKEN
  • Allow Github Actions to create and approve pull requests

Screenshot from 2023-01-15 14-31-30

Set up NPM access tokens

Next step is to generate an NPM access token. If you already have an automation token, skip the creation step and use it (provided that you have saved it somewhere).

Navigate to npmjs.org, and log in if you're not already. Now go to:

Profile PictureAccess TokensGenerate New TokenClassic Token

Now when generating your NPM token give it any name (Because I'll be using this token only for publishing my ESLint pluggin repo, I named it as my repository's name).

Also, save it as an Automation token. (Automation tokens skip 2FA in case it is enabled for your account. Because of that, it's also recommended not to use this token for anything else but CI actions)

Now generate it by clicking on the Generate Token button.

Screenshot from 2023-01-15 14-45-07

At this point, the token will be shown. Copy it and save it somewhere safe, because you won't be able to see it again.

Set up Github Actions Secrets

Now, we need to store this generated NPM token in our repository's Actions secrets. For that go back to your repository and then navigate to:

SettingsSecrets and variablesActions

In this view, hit the New repository secret button. You'll be presented with the "New secret" form.

Now, let's name this secret as NPM_TOKEN and the secret should be the NPM token you generated in the previous step.

After all is done, save the secret by hitting then Add secret button

Screenshot from 2023-01-15 14-49-49

Set up the Changesets Release Action

As mentioned before, we're going to be using Changesets Release Action which will handle:

  • Semantic Versioning
  • Github Releases
  • Tags
  • NPM package publishing

But first we need to add @changesets/cli as a dev-dependency in our repo.

npm i -D @changesets/cli

Let's also create a new script in our project's package.json. We'll call it changeset (I know, very original name, better name suggestions are welcome):

"scripts": {
  "changeset": "changeset"
},

Next step is to change the privacy of the NPM module we want to publish. We need to edit the .changeset/config.json file and set the access property to "public":

{
  ...
  "access": "public"
  ...
}

Finally, we need to initialize changesets in our repo, by running:

npx changeset init

This will create a .changeset/ folder in our project's root directory. We need to include this folder, and any file that will be inside of it, now or in the future, to our repository. Because of that, we should not ignore this folder.

We'll talk a bit more about this folder in the next section.

Set up Publish Github Action

Now that everything's prepared, we need to add a new Github Action to handle all these things automatically for us. For that, let's create the following folder structure in our repo's root directory:

.github/
  workflows/
    publish.yml

As seen in the example from above, we'll add a new file called publish.yml (You can choose a different filename)

Now the fun part. Let's edit publish.yml.

The complete .yml file will be posted at the end of this section.

First, let's give it a name:

name: Publish

Next we'll add is when do we want this file to be executed. In my case, I want it to be executed every time we push something onto my main branch, which is main:

on:
  push:
    branches:
      - main

Optionally, we can add a concurrency option to prevent many release actions to be executed at the same time:

concurrency: ${{ github.workflow }}-${{ github.ref }}

Now let's add the jobs we want to be executed. In this case, we'll use a single job we'll call publish, but you can choose a different name:

jobs:
  release:
    runs-on: ubuntu-latest

Let's now add the steps this job will execute, in the following manner (Next steps examples will not show with the proper indentation, but they will need to be properly indented):

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Step No.1
        # Step No.1 data

      - name: Another step
        # Step No.1 data

      # ... And so forth ....

First step will be to checkout to our branch. For that, we'll use:

- name: Checkout
  uses: actions/checkout@v3

Second step will be to setup node. You can select the node version you'd like to use. In my repository I'm using Node v16.

An important step will also be to specify the registry URL we want to use. In this case, because we want to publish our packet to NPM, well use the NPM registry URL: https://registry.npmjs.org

- name: Setup Node.js 16.x
  uses: actions/setup-node@v3
  with:
    node-version: 16.x
    registry-url: https://registry.npmjs.org

Next we'll install our NPM dependencies (We need to use our changesets dependency in the following steps). We'll use npm ci for this step

- name: Install dependencies
  run: npm ci

After that we need to setup our action to use our stored NPM_TOKEN secret. By default, changesets/action should use the project's .npmrc data when publishing, but there seems to be an active bug that prevents this from happening.

** This next step is very important, otherwise publish won't work**

- name: Setup .npmrc
  run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc

Now comes the main part of this action, using changesets/action. We'll add the next step and we'll explain what's going on here:

- name: Create release PR or publish to NPM
  uses: changesets/action@v1
  with:
    version: npx changeset version
    publish: npx changeset publish
    commit: 'chore(release): changesets versioning & publication'
    title: 'Changesets: Versioning & Publication'
    createGithubReleases: true
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Ok, so what's going on here?

The first step is obvious, use changesets/action@v1 to run this step.

Because we want changesets to handle the semantic versioning, we provide the

with:
  version: npx changeset version

option.

What this will do is: Every time we push something to our main branch, it'll try to look up for major, minor and patch changes and bump the project's version accordingly.

Documenting contributions

But in order for changesets to do this, we need the contributors to also provide the correct changelog when creating a Pull Request. That's why we'll add to our "Contributing" section in our main README.md that whoever contributes needs to run npm run changeset and push those changes to their PR.

Here's an example of a readme that includes this information: https://github.com/egdelgadillo/eslint-plugin-require-ts-check#contributing

Whenever the contributor runs npm run changeset they will be asked what kind of a change this is; whether this is a major, minor or patch change, and also ask the contributor to provide a brief description of what this change is all about. When this step is done, it will then create an .md file in the .changeset/ folder.

The contents of this file will be used to determine how to bump the project's version (major, minor or patch).

Also, we can have many of these files at the same time. e.g. Many contributors have created PRs and we saved them onto a intermediary branch, such as develop. When we decide to merge develop into master, changeset will determine which of this files is the major change and bump the version accordingly.

changeset also updates the CHANGELOG.md file automatically for us. That is, it will grab the description of all .md files inside of the changeset/ folder and add them all to the release message.

Publish Job: First run

At this point we can't help but looking at the name of this job. It's a two-part name:

  • Create release PR

or

  • Publish to NPM

This is because the publish job will be run twice.

The first time this jobs is ran is when we decide to merge something onto our main branch. But this will not create a new release nor publish the package onto NPM. Instead of that, it will automatically create a new branch and create a Pull Request for it, and point it to our main branch.

The name of the automatically created branch, by default, will be changeset-release/main.

This automatically created PR will contain all changes from the contributor plus a new commit that bumps the version and updates CHANGELOG.md.

The name of the commit is determined by the

with:
  commit: 'chore(release): changesets versioning & publication'

option from the release step from above; whilst the name of the automatically created PR will be determined by the

with:
  title: 'Changesets: Versioning & Publication'

option from the same step:

Screenshot from 2023-01-15 15-37-34

This will be done the first time the Create release PR or publish to NPM step is executed. Meaning that the first time this step is run, it won't publish anything to NPM. Rather than that it will just bump the version, update the CHANGELOG.md file and create the Changesets: Versioning & Publication pull request.

Also, as a last step, it will delete all .md files that were added by the contributors to the .changeset/ folder.

All these changes can be made by the action, because we provided the GITHUB_TOKEN option, which lets the bot update the repository, as we added this permission in the Setting up your repository permissions section:

env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Then it will stop.

How does changeset know to just create a PR and not publish the package?

It checks the .changeset folder for any newly added .md file. If it finds any, it will create a PR instead of publishing.

And because the first time the publish action is ran it will delete all contribution .md files, then whenever we merge the automatically created Pull Request, the publish job will be ran for the second time

Publish Job: Second and last run

As stated before, whenever we merge the automatically created PR, this job will be triggered for a second time (Because the PR is trying to merge the changeset-release/main onto main, which triggers the action as configured in the YAML file)

Now, because this time there are no .md files, changeset knows the bump is already done, the CHANGELOG.md has already been updated. Now it's time to:

  • Create a new Github release
  • Add a new tag
  • Publish the new package version to NPM

This is all possible because we provided the

with:
  publish: npx changeset publish
  createGithubReleases: true

options.

All these steps also require the following environment variables:

env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Once this step is ran for the second time, a new Release will be added to our repository. The name of this release will be the new bumped version, and the description will contain all changes added by the contributors (extracted from the .changeset/*.md files):

Screenshot from 2023-01-15 16-00-19

As a very similar step, a new tag for the newly bumped version will be created. The data for the tag will be exactly the same as the one that was used to create the release, so both the release and the tag will share the same data.

Finally, the package will be published to NPM

Complete publish.yml

name: Publish

on:
  push:
    branches:
      - main

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node.js 16.x
        uses: actions/setup-node@v3
        with:
          node-version: 16.x
          registry-url: https://registry.npmjs.org

      - name: Install dependencies
        run: npm ci

      - name: Setup .npmrc
        run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc

      - name: Create release PR or publish to NPM
        uses: changesets/action@v1
        with:
          version: npx changeset version
          publish: npx changeset publish
          commit: 'chore(release): changesets versioning & publication'
          title: 'Changesets: Versioning & Publication'
          createGithubReleases: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Conclusion

Using changeset + Github Actions is an easy way to automate:

  • Semantic Versioning
  • Github Releases
  • Tags
  • NPM package publishing

in your public repository

Contributing

If you have any suggestions on how to improve this guide, you can message me. My email is on my Github profile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment