Automating Versioning and Deployment to npm
Recently I built and deployed a new package to the npm package repository. In this post I’ll describe how to automate the process of building, versioning and publishing node packages to npm using GitHub Actions and Changesets
While building, versioning and deployment of packages can be done locally it takes time and the manual process can be error prone. Automating the process means CI/CD can handle the tasks of ensuring a package is tested and built properly before versioning and deploying it for us. This post presumes some familiarity with package.json
and the node build process. All artifacts and implementation can be seen on the public repo I implemented this approach on.
Controlling Package Content
Before publishing we need to determine what’s going to be included in the package. To do this, build the project and then run:
npx npm-packlist
By default, the output might look something like this:
babel.config.js
dist/buildVariables.js
dist/Config.js
dist/generateHttpFile.js
dist/preset.js
.changeset/config.json
package.json
.vscode/settings.json
.vscode/tasks.json
tsconfig.json
CHANGELOG.md
.changeset/README.md
README.md
src/buildVariables.test.ts
src/buildVariables.ts
src/Config.ts
src/generateHttpFile.ts
src/preset.ts
codegen.yml
.github/workflows/pr.yml
.github/workflows/release.yml
It’s excluded node_modules
and anything else in .gitignore
but there’s still way too much in there. One option is to use .npmignore
but this approach means duplicating what’s inside .gitignore
and keeping .npmignore
updated every time you add something you don’t want published.
I only want to ship the items in dist
so a better approach is to use the files
field of package.json
.
{
...
"files": ["dist"]
...
}
Manual Deployment And Versioning
The name
field in your package.json
controls whether your package is scoped or unscoped and the publish process is slightly different for each. Unscoped packages are named in the format my-package
and are always publicly accessible. Scoped packages are named in the @my-user-or-org/my-package
format and may be publicly or privately accessible. Privately accessible packages requires an npm paid subscription.
To deploy to the npm repository you’ll need an account on npmjs.com. Login with your account credentials using:
npm adduser
Once authenticated, build your project (npm run build
or similar). For unscoped packages, deploy to npm with:
npm publish
Scoped packages default to private access, so deploy a publicly accessible scoped package with:
npm publish --access public
If you receive a 403 Forbidden result on your first publish, it’s likely you haven’t yet verified your email address on npmjs.com. 403 will also occur if you attempt to publish the same version of your package twice. To bump your version number using semantic versioning run:
npm version patch
This will increment your package.json
’s version
field, allowing you to publish
again.
Automating The Process
We’ll use Changesets to automate publishing to npm using GitHub Actions. Changesets will also automate versioning and the creation of a CHANGELOG
for us. The changeset tool can be added to our project with:
npm install -D @changesets/cli && npx changeset init
Check the .changeset/config.js
generated - amongst the other fields, we’ll likely need to change those following from their default. When publishing to a public
npm package, you may see Unexpected end of JSON input if access
is set to its default private
.
{
...
"access": "public", # defaults to private
"baseBranch": "main", # defaults to master
...
}
Any time we make a change to the repo that will alter how the package operates, we can add a changeset using:
npx changeset
Doing so will prompt us for whether the change is major, minor or patch per Semantic Version and ask for a description of the change. This creates a .changeset
file that we’ll commit to source and will be used later to automate the creation of a CHANGELOG
and bump the package version. Installing the changeset bot to our repo will remind us on each Pull Request (PR) whether a .changeset
file was committed or not. The versioning and publishing automation is taken care of by a Github Action that uses these changesets.
Add a workflow to your project under .github/workflows/release.yml
that looks like the one below.
name: release
on:
push:
branches:
- main
jobs:
release:
name: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- uses: actions/setup-node@master
with:
node-version: 14.x
- run: yarn
- run: yarn test
- id: changesets
uses: changesets/action@master
with:
# This expects you to have a script called release which does a build for your packages and calls changeset publish
publish: yarn release
env:
GITHUB_TOKEN: ${{ secrets.CHANGESET_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
This workflow will, for any PR merged (or commit pushed) to main
, raise an automated PR named Version Packages containing the following changes:
- Remove any
.changeset
files - Bump
package.json
’sversion
based on the the collated.changeset
files - Append to the
CHANGELOG
based on the collated.changeset
files
If the Version Packages PR is left open and other merges to main
occur, then the PR will be updated to include any further .changeset
files from those merges. If we merge the PR then our publish
script will execute and the updated, versioned package will be published to npm.
An important difference between the above workflow and the one specified in changesets/action docs is the usage of secrets.CHANGESET_TOKEN
, and only applies if you have automated checks running on your PRs. PRs created using secrets.GITHUB_TOKEN
don’t trigger other workflows. If we have PR checks protecting our branches they will not run if triggered by GITHUB_TOKEN
, and if they don’t run (and pass) we won’t be able to merge the Version Packages PR. The fix is to create a Personal Access Token (PAT) on our GitHub account with repo
scope, add it as a secret to the repository, and have the workflow use our PAT instead of GITHUB_TOKEN
.
The NPM_TOKEN
can be generated via npmjs.com > Profile > Access Tokens and also added as secret to our GitHub repository.
Conclusion
Any PR merged to main
or any commit pushed to main
that includes a .changeset
will create or update the Version Packages PR. Merging Version Packages versions and releases our package to npm.
For modifying the CHANGELOG
format and other configurations check out the changesets docs.