Guide + free tool

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

  1. .github/workflows/release-please.yml

    Runs 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: release
    
  2. release-please-config.json

    Carries 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": {
        ".": {}
      }
    }
    
  3. .release-please-manifest.json

    The 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 write permissions are exactly the ones the README lists. Copy them as they are.
  • release-type tells it which files hold the version. Setting it in the workflow is the simple mode. Leave it out and the action looks for release-please-config.json instead.
  • id: release is there so later steps can read outputs like steps.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-run

The 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--frogdb

With 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-As footer in a commit body forces the version. The README’s example is an empty commit:
    git commit --allow-empty -m "chore: release 2.0.0" -m "Release-As: 2.0.0"
    The CLI takes the same thing as --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.json before 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-version config 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: node

With 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": true gives 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 plain v1.2.3, set "include-component-in-tag": false or 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-workspace and cargo-workspace plugins 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.json

No 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.

Release Please: the GitHub Action, monorepos and a generator