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
- Set up NPM access tokens
- Set up Github Actions Secrets
- Set up the Changesets Release Action
- Set up Publish Github Action
- Complete
publish.yml
- Conclusion
- Contributing
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 Settings → Actions → General. 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
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 Picture → Access Tokens → Generate New Token → Classic 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.
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.
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:
Settings → Secrets and variables → Actions
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
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.
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 }}
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.
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.
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:
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.
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
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):
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
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 }}
Using changeset
+ Github Actions is an easy way to automate:
- Semantic Versioning
- Github Releases
- Tags
- NPM package publishing
in your public repository
If you have any suggestions on how to improve this guide, you can message me. My email is on my Github profile