Release Please, and the release PR it keeps open for you
How the release PR works, the workflow file (there’s a generator below), monorepos, and why your first release came out as 1.0.0. The examples are real dry-run output from a test repo, including the part where the changelog listed the same fix twice.
Definition
What release-please is
release-please is a release tool from the googleapis org on GitHub that reads your conventional commits, works out the next version, and keeps a pull request open that bumps the version and updates CHANGELOG.md. That pull request is the release PR. Every push to your main branch updates it. When you want to ship, you merge it, and release-please tags the commit and creates a GitHub Release. Nothing gets released until a person presses merge, which is the reason to pick it over tools that release on every push. You run it through a GitHub Action, googleapis/release-please-action, which is on v5 now. It has release types for Node, Python, Rust, Go, Java and more, and each one knows which file holds the version. Its own README says it doesn’t publish to package managers, so an npm publish is a step you add yourself.
Free tool
Generate the config
Pick your release type and branch and copy the files. Leave the “still below 1.0.0” box ticked and you get two extra JSON files, because bump-minor-pre-major can’t go in the workflow. It only lives in the manifest config. Tick “Monorepo” to list package directories. Tick “Publish to npm” and the publish steps go in the same job, which matters for a reason covered further down.
Release config generator
.github/workflows/release-please.ymlRuns on every push to your release branch. It opens or updates the release PR; merging that PR cuts the release.
on: push: branches: - main permissions: contents: write issues: write pull-requests: write name: release-please jobs: release-please: runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v5 id: releaserelease-please-config.jsonCarries bump-minor-pre-major, which the workflow cannot. Once you reach 1.0.0 you can delete it and put release-type back in the workflow.
{ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "release-type": "node", "bump-minor-pre-major": true, "packages": { ".": {} } }.release-please-manifest.jsonThe version you are on now. release-please takes it from here.
{ ".": "0.1.0" }
Checked on 2026-09-23: each tool loaded its generated config and cut a test release from it, and every file this form can produce parses. If an action has shipped a newer major since, bump the number after the @.
Setup
The minimal workflow
This is the one-package version from the action’s README, with the version bumped to @v5. It goes in .github/workflows/release-please.yml:
on:
push:
branches:
- main
permissions:
contents: write
issues: write
pull-requests: write
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v5
id: release
with:
release-type: node- It runs on every push to
main. There’s no separate release trigger. Merging the release PR is itself a push to main, and that’s the run that tags and releases. - The three
writepermissions are exactly the ones the README lists. Copy them as they are. release-typetells it which files hold the version. Setting it in the workflow is the simple mode. Leave it out and the action looks forrelease-please-config.jsoninstead.id: releaseis there so later steps can read outputs likesteps.release.outputs.release_created, which is true only on the run that actually cut a release.
One setting lives outside the file. If the action can’t open the PR, go to the repository’s Settings, then Actions, then General, and tick “Allow GitHub Actions to create and approve pull requests”. The README flags it for repositories in an organization.
After that, the PR carries an autorelease: pending label while it waits and gets autorelease: tagged once the release exists. Those labels are how release-please tracks its own state, so leave them alone. Mostly. See the errors section.
You can see what it would do before you commit anything. The CLI has a --dry-run flag that prints the release PR it would open and touches nothing. This is how I ran it against a test repo:
npx release-please release-pr --token="$(gh auth token)" --repo-url=OWNER/REPO --release-type=node --target-branch=main --dry-runThe first question everyone has
Which version the first release gets
My test repo had no tags, no releases, a feat, a fix, and a package.json saying 1.0.0. The dry run:
‼ No version for path .
...
Would open 1 pull requests
fork: false
title: chore(main): release 1.0.0
branch: release-please--branches--main--components--frogdbWith nothing to start from, release-please proposes 1.0.0. That’s the default in its source, and it doesn’t read the number out of package.json. My file happened to say 1.0.0 as well, so the log line was the slightly absurd updating from 1.0.0 to 1.0.0. If your project is at 0.4.2 and you have never tagged it, you still get a 1.0.0 PR.
There are three ways to get the number you meant:
- A
Release-Asfooter in a commit body forces the version. The README’s example is an empty commit:
The CLI takes the same thing asgit commit --allow-empty -m "chore: release 2.0.0" -m "Release-As: 2.0.0"--release-as. I ran the dry run again with--release-as=0.1.0:‼ Setting version for . from release-as configuration title: chore(main): release 0.1.0 √ updating from 1.0.0 to 0.1.0 - In manifest mode, write the version you’re on into
.release-please-manifest.jsonbefore the first run. The generator does that from the “version you are on now” field. release-please treats it as the last release and bumps from there. - The
initial-versionconfig key replaces the 1.0.0 default for a package that has never been released.
Below 1.0.0, bump-minor-pre-major makes a breaking change bump the minor number instead of jumping to 1.0.0. It’s off by default. Whether you want it is a semantic versioning question more than a release-please one.
The changelog in that same dry run is the other thing worth looking at:
## 1.0.0 (2026-09-23)
### Features
* build FrogDB field guide and admin with 35 frogs ([d9fde14](https://github.com/mergetel/frogdb/commit/d9fde1449eef65ada4d24987fc468e8a26a06eff))
### Bug Fixes
* use sharper portraits for large frog images ([b09ebe8](https://github.com/mergetel/frogdb/commit/b09ebe8e7b304477199f2b9c8a471cd997c8bb02))
* use sharper portraits for large frog images ([ec21cca](https://github.com/mergetel/frogdb/commit/ec21cca624ff9ff95406059beeea3acae6332972))One fix, listed twice. The repo used GitHub’s merge commits, and GitHub puts the PR title in the merge commit’s body. release-please reads every conventional line in a body as a commit of its own, so it counted the branch commit ec21cca and the merge commit b09ebe8. Its README recommends squash merging, and this is the practical reason. If a bad entry already shipped, a BEGIN_COMMIT_OVERRIDE / END_COMMIT_OVERRIDE block in the merged PR’s description replaces the message on the next run. The README says that one doesn’t work with plain merge commits either.
The one that confuses people
Why CI doesn’t run on the release PR
By default the action uses the built-in GITHUB_TOKEN. GitHub doesn’t start new workflow runs for events that token causes, so it can’t loop forever. So the release PR opens and your test workflow never runs on it. If your branch rules require those checks, they never report. Same for the tag and the release. The README says workflows that normally trigger on a release being created won’t run either.
The fix the README gives is a personal access token saved as a secret and passed as token:
- uses: googleapis/release-please-action@v5
id: release
with:
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
release-type: nodeWith a PAT, CI checks run on the release PR. The cost is a token tied to a person. If you only needed the release event to publish, skip the PAT and do what the generator does: put the publish steps in the same job, each gated on if: ${{ steps.release.outputs.release_created }}. No second workflow, so there’s nothing to trigger.
Several packages, one repo
Monorepos and manifest mode
A release please monorepo setup drops release-type from the workflow and moves the settings into two files at the repo root. Here’s what the generator makes for two packages:
release-please-config.json says which directories are packages:
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "node",
"bump-minor-pre-major": true,
"packages": {
"packages/api": {},
"packages/web": {}
}
}.release-please-manifest.json holds the last released version of each. You set it once. After that, every release PR updates it:
{
"packages/api": "0.1.0",
"packages/web": "0.1.0"
}- By default you get one combined release PR covering every package with changes.
"separate-pull-requests": truegives you one per package. - Settings at the top of the config are defaults, and any package can override them, including
release-type. A Rust crate and an npm package can share a repo. - Tags look like
<component>-v<version>. If your old tags are plainv1.2.3, set"include-component-in-tag": falseor release-please won’t find them. - Outputs get the path as a prefix. With a slash in the path you need bracket syntax:
if: ${{ steps.release.outputs['packages/api--release_created'] }} - If packages depend on each other through npm or Cargo workspaces, the
node-workspaceandcargo-workspaceplugins keep those internal version ranges in step.
Manifest mode isn’t only for monorepos. A single package at "." works the same way, and it’s the only place the action takes options like bump-minor-pre-major. That’s why the generator switches to it when you tick the pre-1.0 box.
Upgrading
Moving from v4 to v5
release-please-action v5.0.0 came out on 2026-04-22. The one breaking change in its release notes is the move to the node24 runtime. The same release bumped the bundled release-please to 17.6.0. For most people the upgrade is changing @v4 to @v5. The README examples still say @v4, which is how you end up copying the old one.
The bigger jump was v3 to v4, if you’re coming from something older. v4 removed the command input and moved most options into the manifest config. command: manifest became “don’t set release-type”, and command: release-pr became skip-github-release: true.
Troubleshooting
Errors you’ll hit
“Missing required manifest config”
I got this by leaving --release-type off the dry run against a repo with no config file:
ConfigurationError: base (mergetel/frogdb): Missing required manifest config: release-please-config.jsonNo release-type means manifest mode, and manifest mode needs the file. Either put release-type back in the workflow or commit release-please-config.json. There’s a sibling, Missing required manifest versions, for when the config exists and .release-please-manifest.json doesn’t. For the first run, an empty {} is enough.
“No user facing commits found”
The full line is No user facing commits found since <sha> - skipping, and no PR opens. This one is working as designed. release-please only opens a PR when the changelog would have something in it: a feat, fix, perf, revert or a breaking change. A week of chore and docs is hidden from the changelog, so there’s nothing to release. If you want a release anyway, use a Release-As footer.
“commit could not be parsed”
> commit could not be parsed: b09ebe8e7b304477199f2b9c8a471cd997c8bb02 Merge pull request #1 from mergetel/fix/sharper-frog-portraits
> error message: Error: unexpected token ' ' at 1:6, valid tokens [(, !, :]That’s from my dry run, and it’s harmless. A merge commit titled “Merge pull request #1” isn’t a conventional commit, so the title is skipped. The body still gets read, which is where the duplicate changelog entry came from. If the commit it names is one of yours, fix the format for next time. The cheat sheet has it on one screen.
“Expected 1 releases, only found 0”
On a first run this is normal, and my test repo printed it because it had no releases. After you’ve released, it means release-please is searching for a tag name that doesn’t match yours. The usual cause is the monorepo tag format against old v1.2.3 tags, and include-component-in-tag fixes it.
The release PR never comes back
If an old release PR still has the autorelease: pending label, release-please thinks a release is in flight and won’t open another. The README says a failed API call can leave the label behind. If you’re sure nothing is pending, remove the label and re-run the workflow.
Choosing
release-please or something else
semantic-release reads the same commits and releases on every push that warrants it, with no PR in between, and it can publish to npm in the same run. If nobody needs to approve a release, it’s less to click. Changesets ignores commit messages and has each contributor write a small file describing their change, then opens a version PR much like this one. It fits JavaScript monorepos where the person making a change should say how big it is.
release-please sits between them. The version comes from commits, and a human still decides when. It also ships release types for Python, Rust, Go and Java, so a repo with no npm in it works fine. If all you want is a nicely formatted changelog file, the changelog format guide covers what goes in one.
Honesty
What this doesn’t fix
Merge the release PR and you get a tag, a GitHub Release and a tidy CHANGELOG.md. All of it sits inside the repo. People who watch the repo might see it. Everyone else finds out when you tell them, and a changelog line like “use sharper portraits for large frog images” is fine in a changelog and flat as a post.
That gap is why I built Merge & Tell. It reads each merged pull request, the diff and not just the title, and drafts a post for each network. I read every one before it goes out. The updates page is that running on this product.
The release PR is merged. The announcement isn't.
You were going to merge the PR anyway.