Skip to content

Repository files navigation

Checks

ExodusMovement/lerna-release-action

Action that allows selectively releasing packages in a lerna monorepo. It works around lerna's opinionated releasing of every dependant if a package changed. This comes with its own dangers and should be used with caution. The action creates a release PR that is assigned to the user that dispatched the workflow.

Version workflow

name: Version
on:
  workflow_dispatch:
    inputs:
      packages:
        description: 'Selected packages as comma separated string, e.g. @exodus/storage-spec,@exodus/formatting or just storage-spec,formatting'
        type: string
        required: true

jobs:
  version:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v3
        with:
          node-version-file: '.nvmrc'
      - name: Enable package manager
        run: corepack enable
      - name: Install dependencies
        run: corepack pnpm install --frozen-lockfile
      - uses: ExodusMovement/lerna-release-action/version@master
        name: Version
        with:
          github-token: ${{ secrets.GH_AUTOMATION_PAT }} # should not be the default GITHUB_TOKEN, otherwise subsequent automation will not run (such as checks on the release PR)
          packages: ${{ inputs.packages }}

Enabling auto-merge

The Version action can enable auto-merge on the created pull request. This is possible when the repository allows auto-merge, squash merging is enabled and has branch protection rules set up.

To use it, set the auto-merge input of the action to true.

- uses: ExodusMovement/lerna-release-action/version@master
  name: Version
  with:
    # other inputs here
    auto-merge: true

Labelling the release PR

The Version action always labels the release PR with publish-on-merge and one label per released package. To apply additional labels, pass them as a comma-separated string via the labels input.

- uses: ExodusMovement/lerna-release-action/version@master
  name: Version
  with:
    # other inputs here
    labels: release,automated

Publish workflow

name: Publish
on:
  push:
  workflow_dispatch:

jobs:
  publish:
    if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, format('chore{0} release', ':'))
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: read
    steps:
      - name: Resolve release ref
        id: release-ref
        uses: ExodusMovement/lerna-release-action/release-ref@master
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - uses: actions/checkout@v4
        with:
          ref: ${{ steps.release-ref.outputs.sha }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Enable package manager
        run: corepack enable
      - name: Install dependencies
        run: corepack pnpm install --frozen-lockfile
      - name: Build
        run: corepack pnpm run build
      - name: Publish
        uses: ExodusMovement/lerna-release-action/publish@master
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

On a push of a release commit, publish releases the head of the merged release PR, not the pushed commit. This way it does not publish changes that merged after the release PR. release-ref resolves that commit before the checkout, so the install and build steps run on the tree that gets published. When HEAD is already at the release PR head, publish does not check out again.

Without release-ref, publish checks out the release PR head itself, after the install and build steps. The installed dependencies and the build output then come from the pushed commit. In a pnpm workspace with verifyDepsBeforeRun, pnpm can also stop the publish, because the manifests no longer agree with the last install.

When refreshing the lockfile after selective versioning, the action prefers the repository's declared package manager from the root package.json#packageManager or lerna.json#npmClient. If neither is present, it falls back to lockfile detection.

Publishing uses the same detection and runs Lerna through pnpm exec in pnpm workspaces, keeping npx for other workspaces. This lets pnpm validate installed dependencies before Lerna rewrites workspace references for packing. Publish hooks then inherit pnpm's validated execution context instead of checking the temporary manifests against the original lockfile. Dependency verification and publish hooks remain enabled.

Version dispatch workflow

Automatically start versioning of packages when a PR is merged. Requires using ExodusMovement/lerna-package-name-action to label PRs, as these package labels will be used to determine the packages to be versioned.

name: Version dispatch
on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - edited # required: the PR title gates the release, so title edits must re-run this
      - labeled
      - unlabeled
      - closed

jobs:
  invoke-versioning:
    if: contains(github.event.pull_request.labels.*.name , 'publish-on-merge') == false && contains(github.event.pull_request.labels.*.name , 'skip-release') == false
    name: Invoke version workflow
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ExodusMovement/lerna-release-action/version-dispatch@master
        with:
          version-workflow-id: version.yaml
          github-token: ${{ secrets.GH_AUTOMATION_PAT }}
          exclude-commit-types: chore,docs,test,ci

PR title gates the release

A PR whose title carries no release-worthy conventional-commit type (chore:, docs:, refactor:, or a non-conventional subject) releases nothing, whatever its individual commits say — no version dispatch on merge, and no version preview comment while the PR is open.

The reason is squash merging: the PR title becomes the commit on the default branch, and changelogs are generated from that history. A chore: title cannot produce a changelog entry, so releasing on the PR's commits would publish a version whose changelog reads **Note:** Version bump only for package ….

To release from a PR, give it a feat:, fix:, perf: or breaking (!) title. The title decides whether anything releases at all, and commit-level types decide which packages bump. The commits also decide by how much, unless the title is breaking.

Because the title is part of the gate, include edited in the workflow's types: — otherwise retitling a PR leaves a stale preview comment (or a missing one) until the next push.

A breaking PR title releases a major

A ! in the title promotes every package the PR releases to major, whatever level its individual commits carry. Squash merging is again the reason: the title is the commit that lands on the default branch, so its ! is what the generated changelog and every consumer see. A feat(x)!: title over a plain feat(x): commit would otherwise cut a minor whose changelog carries a BREAKING CHANGES section, and consumers would resolve the incompatible version through a range that still matches it.

The title never widens the release set. A package that no releasing commit touched stays unreleased, because only the commits carry the per-package file attribution.

Running from a subdirectory

All three actions (version, publish, version-dispatch) accept an optional path input — the working directory to run in, relative to the checkout root. Set it when the lerna workspace lives in a subdirectory of the repository rather than at its root:

- uses: ExodusMovement/lerna-release-action/version@master
  with:
    github-token: ${{ secrets.GH_AUTOMATION_PAT }}
    packages: ${{ inputs.packages }}
    path: apps/mobile

The actions still create commits, tags and PRs against the whole repository — path only scopes where lerna, the package manager and workspace discovery run. When omitted, the actions run at the checkout root exactly as before.

About

Selectively release packages from a lerna monorepo

Resources

Stars

1 star

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages