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.
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 }}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: trueThe 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,automatedname: 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.
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,ciA 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 ! 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.
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/mobileThe 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.