A step closer to the perfect npm CD pipeline?
In my last post, I detailed my typical CD pipeline for deploying npm packages. At the end of the article, I outlined a caveat to bear in mind when using it: the newly published version will always have the latest
tag. This is an issue because whenever someone installs a package without specifying a tag or version, the npm CLI will default to the latest
tag.
The issue in depth
This:
npm install awesome-package
Is shorthand for
npm install awesome-package@latest
If you publish version v1.0.0 and then v2.0.0, the latest
tag will initially be attached to the v1.0.0 release but will later be reassigned to the v2.0.0 release. So far, this is desired behaviour.
If/When a critical bug gets discovered in your v1.0.0 release, you might want to patch it with a v1.0.1 release, especially if many users are still on v1 and the migration to v2 is tricky. When your CD pipeline, as outlined in my previous post, runs, it will assign the latest
tag to that v1.0.1 patch release; now, anyone who runs npm install awesome-package
will get v1.0.1, rather than v2.0.0.
Note that the same can be said for releases that are done manually via the CLI or by any other CD process which doesn’t factor in npm registry tags.
The manual fix
The solution to this issue is to assign a tag to your new releases as you release them; if the npm publish
command has a tag supplied to it with the --tag
flag, then it won’t automatically apply the latest
tag.
So what do you use for tags? Well, npm suggest you don't use version numbers as tags because they share the same domain as each other already when users install packages:
npm install awesome-package@my-tag
npm install [email protected]
My solution is to explicitly provide the latest
tag for new releases to your current major version and apply a latest-X
tag to patches of previous major versions, where ‘X’ is that major version number.
For example, if you have a v2.0.0 already released and:
- you’re publishing v2.0.1: then you give it the tag
latest
- you’re publishing v1.0.1: then you give it the tag
latest-1
Assuming you’re following the SemVer specification, this approach has the added benefit that a consumer of your package can run npm awesome-package@latest-X
with their current major version number and be confident that they’ll get the latest and greatest version of your package without any breaking changes in it.
The Automatic GitHub Action fix
For those of us who deploy our npm packages via CD pipelines, we’re going to need a programmatic approach to apply this solution for us. I’ve written an npm package called npm-publish-latest-tag that can be used to generate these tags when given the path to a package.json file.
Internally, this package:
- Retrieves your version from your package.json file
- Retrieves your latest version from the supplied npm registry
- Compares the two to either return
latest
orlatest-X
This package is available on npmjs.com and you’re welcome to use it, however, if you also use GitHub Actions then I’ve also wrapped this npm package in a GitHub Action step which I suggest you use instead.
You can amend an existing GitHub Action file by running the Action step linked to above and then passing the outputted value into your npm publish
command.
steps:
- uses: actions/checkout@v2
# [ ... ]
- uses: tobysmith568/npm-publish-latest-tag@v1
id: latest_tag
with:
package-json: ./package.json
# [ ... ]
- run: npm publish --tag ${{ steps.latest_tag.outputs.latest-tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
And there you go!
Hopefully, we’re now all one step closer to that perfect npm CD pipeline.
Extra benefits
An extra benefit of using my npm package or GitHub Action step is that it handles pre-release sections of versions too. No matter if it’s your current major version or not, if you publish a version that has a pre-release section then you’ll get a tag that looks like latest-X-Y
where ‘X’ is the major version and ‘Y’ is the pre-release type (alpha, beta, etc). Furthermore, build metadata will be ignored.
For example, if you have a v2.0.0 already released:
- Publishing v2.0.1-beta results in
latest-2-beta
- Publishing v3.0.0-alpha results in
latest-3-alpha
- Publishing v2.0.0+123 results in
latest
- Publishing v2.0.1-beta+123 results in
latest-2-beta
Users can now install @latest
to get your latest stable major version release but can also optionally get the latest version of your 3.X.X-beta
if they wish to without needing to know the exact version.