How to publish on npm with `--provenance` using Lerna-Lite
Tags: provenance, npm, publish
Published: 2023-11-17 05:37:22

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... In this article we also assume that you have already configured Lerna/Lerna-Lite.

### What is Provenance?
I would suggest you read this GitHub blog post [Introducing npm package provenance](https://github.blog/2023-04-19-introducing-npm-package-provenance/) for a more detailed description, below is a quote pulled from that 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](https://github.com/lerna-lite/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 a dependency on Nx, 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.

### Instructions
1. `GITHUB_TOKEN`: Create a personal GitHub access Token (or PAT), go to your GitHub Profile click on "Settings -> Developer Settings -> Personal Access Token -> Classic Token and give it the `repo:public_repo` scope.
2. `NPM_TOKEN `: create an NPM Token for publishing (login on NPM then click on "Access Tokens -> Generate Token -> Classic Token" 
3. GitHub Secret: navigate to your GitHub project to automate and add a secret, click on project "Settings -> General -> (security) Secret and Variables -> Actions"
  - create a new secret as `NPM_TOKEN` and paste your NPM token from previous step.
4. Create NPM Scripts for Version/Publish with Lerna-Lite
5. Create a GitHub Action Workflow (2 use cases are shown below)
6. Enable Provenance via `.npmrc` or within the workflow:
   - in `.npmrc` via `provenance=true` or
   - in the workflow via `env: NPM_CONFIG_PROVENANCE: true`
7. ... and finally, run 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 a portion from this [NPM](https://docs.npmjs.com/generating-provenance-statements#example-github-actions-workflow) link and modified it a bit to make it work with Lerna-Lite, however in my case I opted for the second approach with OTP shown further down.

##### `package.json`
```json
{ 
  "scripts": {
    "ci:publish:latest": "lerna publish --conventional-commits --dist-tag latest --no-verify-access --yes"
  }
}
```

##### `.github/workflows/release.yml`
```yaml
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 with OTP (One Time Password), it is the configuration that we use in Lerna-Lite itself to publish the project with provenance. 

To deal with the OTP (or any other 2FA), we can use [wait-for-secrets](https://github.com/step-security/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 the token is still valid at the time of the publish. 

Another thing that we considered was 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 can 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`
you can also use `NPM_CONFIG_PROVENANCE: true` in your workflow
```
provenance=true
```

##### `package.json`
```json
{ 
  "scripts": {
    "ci:version": "lerna version --yes",
    "ci:publish": "lerna publish from-package --force-publish --yes",
  }
}
```
##### `.github/workflows/release.yml`
```yaml
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: Build Project
        run: pnpm build

      - 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 "${{ github.actor }}@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 Lerna-Lite with Provenance as shown below. You should be able to do the same with your project as well. 

Also note that you can do the same steps with Lerna as well (the actual implementation came from Lerna, so credit goes to them).


![Built and signed
with provenance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ptzzshfiechqoctkl2cd.png)

and at the bottom of the NPM package, you should also see

![Provenance summary](https://github.com/lerna-lite/lerna-lite/assets/643976/54c2bcef-1f62-4718-a94a-b738832887a5)