prod like a pro, npm edition

august 29, 2026.

similar to how i’ve been able to automate how i deploy backend services, i wanted to ensure that when releasing libraries, every step of the way was fully automated.

overall, i wanted the following to hold true:

  1. i work on new changes on the library in a feature branch.
  2. when i raise a pull request from the feature branch to main, a “preview” version of the changes which i just worked on gets published to npm (or whichever registry) - something installable by anyone.
  3. once validated and merged, a “canary” or “next” version gets automatically released to the registry.
  4. once i am ready to “cut” a new version, i create a new release & tag on github, then publishing it properly releases the library in the registry on the latest tag.

after trialing out the above for a bit, it became clear that there was no value in having step 2 since i work solo on the library.

npm link locally was sufficient, so i simplified to just:

  1. on every push/merge to main, release a canary version of the library.
  2. once ready for proper release, release the latest stable version.

canary

published versions under the canary tag are what i use to validate e2e that the functionality i am introducing or the bug that i am fixing works the way it’s intended to.

it’s also a version i can point anyone looking to trial a fix or new feature without breaking the stable version.

for chronos’ sdk, installing the latest canary would just be:

bash
npm i @chronos.sh/sdk@canary

or if looking to eval a specific canary version:

bash
npm i @chronos.sh/[email protected]

to get this happening automatically on merge to main, i have the following action job:

yaml
# snippet from the full workflow
on:
  push:
    branches: [main]

jobs:
  canary:
    name: Publish canary
    runs-on: ubuntu-latest
    environment: npm-publish
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup

      - name: Set canary version
        run: |
          SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
          NEXT_PATCH=$(npm version patch --no-git-tag-version)
          npm version "${NEXT_PATCH#v}-canary.${SHORT_SHA}" --no-git-tag-version

      - run: pnpm build

      - name: Publish
        run: npm publish --tag canary --access public

note: to authenticate with npm without an explicit npm token, i am using npm’s OIDC trusted publishing

latest

once satisfied with whatever i was validating in the canary tag, releasing the package on the latest tag then becomes similar to how i release backend services to prod.

i first bump the version in package.json, then create a release and corresponding tag in github and hit publish.

upon publishing the github release, an action is responsible for automatically publishing the library:

yaml
# snippet from the full workflow
on:
  release:
    types: [published]

jobs:
  release:
    name: Publish stable
    runs-on: ubuntu-latest
    environment: npm-publish
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup

      - name: Validate tag matches package version
        run: |
          PKG_VERSION=$(node -p "require('./package.json').version")
          TAG_VERSION="${GITHUB_REF_NAME#v}"
          if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
            echo "::error::Tag '${GITHUB_REF_NAME}' does not match package.json version '${PKG_VERSION}'"
            exit 1
          fi

      - run: pnpm build

      - name: Publish
        run: npm publish --access public

upon publishing, the latest stable version of the library is then installable via:

bash
npm i @chronos.sh/sdk
— k.