I recently started publishing Lerna-Lite project on NPM with Provenance and decided to write a blog post about the steps since it took me quite a few commits to find the best configuration...
What is Provenance?
For a more detailed explanation, I would suggest you read this GitHub blog post Introducing npm package provenance, below is a quote pulled from the blog:
provenance data gives consumers a verifiable way to link a package back to its source repository and the specific build instructions used to publish it
What is Lerna-Lite?
From Lerna's website, it is described as
Lerna is a fast, modern build system for managing and publishing multiple JavaScript/TypeScript packages from the same repository.
Lerna-Lite is a lighter version of Lerna (every commands are optional in Lerna-Lite) as opposed to Lerna which is a "all-in-one" tool which includes 15 commands. Another difference is that the newest Lerna version has Nx as a dependency, but on the other hand Lerna-Lite does not require neither use Nx. Lastly, Lerna-Lite was created couple months before Nx company took over stewardship of Lerna.
What you will need
- Create an NPM Token for publishing (login on NPM then click on "Access Tokens -> Generate Token -> Classic Token"
- Go to your GitHub project and add a secret, click on "Settings -> General -> (security) Secret and Variables -> Actions"
- add new secret "NPM_TOKEN" and paste your NPM token from previous step.
- Create a GitHub Token, go to your GitHub profile click on "Settings -> Developer Settings -> Personal Access Token -> Classic Token and give
repo:public_repo
scope. - Create NPM Scripts for Lerna version/publish
- Create a GitHub Action Workflow (2 use cases are shown below)
- Enable provenance via
.npmrc
or within the workflow a) in.npmrc
viaprovenance=true
b) or in the workflow viaenv: NPM_CONFIG_PROVENANCE: true
- Execute the workflow
Use Cases
a) Basic Usage
Let's start with a basic usage, we'll create a GitHub Action workflow and publish on the registry using NPM to publish with Provenance.
Note, I have to admit that I did not personally test this use case, I copied some part from this NPM and modified it a bit to demo this use case with Lerna-Lite, however in my case I opted for the second approach with OTP shown further down.
package.json
{
"scripts": {
"ci:publish:latest": "lerna publish --conventional-commits --dist-tag latest --no-verify-access --yes"
}
}
.github/workflows/release.yml
name: Publish Package to npmjs
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v3
with:
node-version: 18
# a registry is required to publish
registry-url: https://registry.npmjs.org/
cache: npm
cache-dependency-path: '**/package.json'
- name: NPM install
run: npm install -g npm
- name: Version & Publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
run: |
git config --global user.name "${{ github.actor }}"
git config --global user.email "users.noreply.github.com"
npm whoami
npm run ci:publish:latest
b) Lerna-Lite's project with pnpm workspace and 2FA (OTP)
For a more extensive and preferred use case which is to use OTP (One Time Password), it is the configuration that we use in Lerna-Lite itself to publish the project with provenance for better security.
To deal with the OTP (or any other 2FA), we can use wait-for-secrets. When comparing this approach with the previous basic usage, we can see that the Lerna-Lite Version & Publish were split into 2 separate tasks. The reason is simple, calling the OTP too early would timeout even before reaching the publish phase, so calling the OTP just before the publish is ideal to make sure that the token is still valid at the time of the publish.
Another thing that is worth considering, though optional, is to add a condition to make sure that the current actor (user currently logged in GitHub) is in the allowed list of users to execute the publish; if not it will simply exit the workflow. You'll need to modify the ["user1", "user2"]
shown below with your username and anyone else that is allowed to publish. We also use workflow_dispatch
to have a prompt to start the workflow, you could also add extra inputs to release a fixed version if you wish.
Note Lerna-Lite/Lerna uses NPM to publish on the registry, even if you set
"npmClient": "pnpm"
(or Yarn), which is actually a good thing since Yarn does not yet support Provenance.
.npmrc
provenance=true
package.json
{
"scripts": {
"ci:version": "lerna version --yes",
"ci:publish": "lerna publish from-package --force-publish --yes",
}
}
.github/workflows/release.yml
name: 🏷️ Publish NPM Latest
on: workflow_dispatch
permissions:
contents: write
id-token: write
jobs:
deploy-npm-latest:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
fetch-depth: 3
token: ${{ secrets.GITHUB_TOKEN }}
- if: ${{ github.event.pull_request.merged != true && contains('["user1", "user2"]', github.actor) != true }}
name: Ensure current actor is allowed to run the workflow
run: |
echo "Error: Your GitHub username (${{ github.actor }}) is not on the allowed list of admins for this workflow"
exit 1
- name: Set NodeJS
uses: actions/setup-node@v4
with:
registry-url: 'https://registry.npmjs.org/'
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Get pnpm store directory
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Run pnpm install dependencies
run: pnpm install
- name: Run all workspace TSC builds
run: pnpm build:full
- name: Lerna Version 🏷️
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
git config --global user.name "${{ github.actor }}"
git config --global user.email "users.noreply.github.com"
pnpm whoami
pnpm run ci:version
- name: OTP
uses: step-security/wait-for-secrets@v1
id: wait-for-secrets
with:
secrets: |
OTP:
name: 'OTP to publish package'
description: 'OTP from authenticator app'
- name: Lerna Publish 📦
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
pnpm run ci:publish --otp ${{ steps.wait-for-secrets.outputs.OTP }}
Conclusion
With that in place, we are now successfully publishing with Provenance using Lerna-Lite which makes our toolchain much more secure. Following these steps, you should be able to do the same with your project as well.
Also note that you can also do the exact same steps with Lerna (the actual implementation actually came from Lerna, so credit goes to them).
For an example, you can visit one of Lerna-Lite's package, like the @lerna-lite/cli package, to find the provenance badge applied on each new versions and that's it!
Top comments (0)