Skip to content
Latchkey LogoLatchkey home

release-please-action: release PRs from Conventional Commits

release-please-action reads your Conventional Commit history and keeps one release pull request open that bumps the version and writes the changelog; merging that pull request tags the release and creates the GitHub release. At googleapis/release-please-action@v5, its release_created output is how a later job publishes only when a release actually happened.

Most teams that adopt release-please want the same three things: no hand-edited version numbers, a changelog nobody has to write, and a publish step that runs exactly once per release. Each of them depends on something outside the action: commit messages that follow the convention, and a workflow that reads release_created before it publishes.

Two things trip people up after that. The action moved to googleapis/release-please-action, and the old google-github-actions/release-please-action repository is archived, so older snippets point at a repository that no longer changes. And with the default token, CI on the release PR waits for approval or does not run at all.

Release-please loop: commits on main, release PR updated, merge, tag and release, publish job on output
One action, two jobs per run: create releases for merged release PRs, then open or update the next release PR. The publish job reads release_created.

How the release PR loop works

Every push to the release branch runs the action, and each run does two things in order. First it checks whether a release PR was just merged and, if so, tags it and creates the GitHub release. Then it reads the commits since the last release and opens or updates the release PR. The version bump comes from the commit types: fix: is a patch, feat: is a minor, and a ! after the type, as in feat!:, is a major.

The workflow needs contents: write, issues: write and pull-requests: write. In an organization you may also need to switch on Allow GitHub Actions to create and approve pull requests under Settings, Actions, General; without it the PR step is refused.

.github/workflows/release-please.yml
name: release-please
on:
  push:
    branches: [main]
permissions:
  contents: write
  issues: write
  pull-requests: write
jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
        id: release
        with:
          release-type: node

The few settings that live in the workflow

Most configuration lives in the config file, not here. These are all the inputs the action accepts.

InputDefaultWhat it does
tokengithub.tokenToken for creating release PRs, tags and releases
release-typenoneStrategy such as node, python, go, rust, simple; unset means use the manifest config
pathrepository rootRelease a package in a subdirectory
target-branchdetectedBranch to open the release PR against
config-filerelease-please-config.jsonPath to the config file
manifest-file.release-please-manifest.jsonPath to the versions manifest
include-component-in-tagfalsePrefix tags and branches with the component name
skip-github-releasefalseOnly open PRs, do not tag
skip-github-pull-requestfalseOnly tag, do not open PRs
skip-labelingfalseDo not label the PR
versioning-strategydefaultHow the next version is computed
release-asnoneForce the next version
forkfalseOpen the PR from a fork; needs a user token, not GITHUB_TOKEN
repo-url, github-api-url, github-graphql-url, changelog-host, proxy-serverthis repository and serverFor GitHub Enterprise and proxies

Config and manifest files

Since v4, nearly every option lives in two files at the root of the default branch. release-please-config.json holds the settings per package, and .release-please-manifest.json records the last released version of each package path. When release-type is not set, the action uses these files; when it is set, the action builds a one-package config on the fly and the files are ignored.

For an existing project, seed the manifest with the current version so release-please does not propose its default starting version, and set bootstrap-sha so the first changelog does not sweep in the whole history. The manifest docs say bootstrap-sha is ignored once the first release PR has merged, so you can delete it afterwards.

release-please-config.json
{
  "bootstrap-sha": "<full sha of the commit before the first one to include>",
  "packages": {
    ".": {
      "release-type": "node",
      "changelog-path": "CHANGELOG.md"
    }
  }
}

Monorepos: one PR, one tag per package

List each package path under packages and its current version in the manifest. By default all packages share one combined release PR; separate-pull-requests: true gives each its own. Set include-component-in-tag so tags read api-v1.4.0 rather than colliding on v1.4.0.

Outputs for a package outside the root are prefixed with its path, so the root release_created becomes packages/api--release_created. A path containing / is not a valid identifier in an expression, so read it with bracket syntax: steps.release.outputs['packages/api--release_created']. The paths_released output is a JSON array of every path released in the run, which the action's source says exists to feed a matrix.

release-please-config.json
// release-please-config.json
{
  "include-component-in-tag": true,
  "packages": {
    "packages/api": { "release-type": "node" },
    "packages/cli": { "release-type": "node" }
  }
}
// .release-please-manifest.json
{ "packages/api": "1.4.0", "packages/cli": "0.9.2" }

Chaining a publish job: compare with 'true'

The outputs are how the publish job knows a release happened: release_created, tag_name, version, major, minor, patch, sha, html_url, upload_url and body for the root package, and releases_created, paths_released, prs_created, pr and prs for the run as a whole.

One of them behaves differently from the rest. In src/index.ts at v5.0.0, release_created is only set when a release was created, so it is empty otherwise, but releases_created is always set, to the string true or false. GitHub's expression rules treat any non-empty string as truthy, so if: ${{ steps.release.outputs.releases_created }} runs every time. Issue #912 on the action reports exactly that. Compare with == 'true' for every output and the question never comes up.

.github/workflows/release-please.yml
jobs:
  release-please:
    runs-on: ubuntu-latest
    outputs:
      release_created: ${{ steps.release.outputs.release_created }}
      tag_name: ${{ steps.release.outputs.tag_name }}
    steps:
      - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
        id: release
        with:
          release-type: node
  publish:
    needs: release-please
    if: needs.release-please.outputs.release_created == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          ref: ${{ needs.release-please.outputs.tag_name }}
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: 22
          registry-url: 'https://registry.npmjs.org'
          package-manager-cache: false
      - run: npm ci
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Why CI does not run on the release PR, and the token fix

With the default GITHUB_TOKEN, the release PR, its tag and its GitHub release are all created by that token, and GitHub does not start new workflow runs from events that token causes. The release-please README still describes it that way. GitHub's own documentation, read 2026-11-12, now carves out one exception on github.com: a pull request opened, synchronized or reopened by GITHUB_TOKEN creates its pull_request runs in an approval-required state, and someone with write access has to click Approve workflows to run. The tag push and the release event still start nothing.

Either way, the fix is the one both documents give: pass a GitHub App installation token or a PAT as token. Then the release PR runs CI like any other pull request, and a separate workflow listening for the tag or the release fires. An App token is the better choice, because it is short-lived and not tied to a person.

.github/workflows/release-please.yml
steps:
  - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
    id: app-token
    with:
      client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
      private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
  - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
    id: release
    with:
      token: ${{ steps.app-token.outputs.token }}
      release-type: node

v4 moved configuration out of the workflow

v4 is where configuration moved out of the workflow; v5 changed only the runtime.

ReleaseChange
v4.0.0TypeScript rewrite, Node 20; command removed in favor of release-type, skip-github-release and skip-github-pull-request; most options moved to the manifest config
v4.2.0skip-labeling input
v4.4.0versioning-strategy and release-as inputs
v5.0.0Node 24 runtime, release-please library 17.6.0

Key takeaways

  • fix: is a patch, feat: a minor, ! a major; merging the release PR is what tags the release.
  • Without release-type, everything comes from release-please-config.json and the manifest.
  • releases_created is always the string true or false; compare with == 'true'.
  • Use a GitHub App token so CI and release workflows run on what release-please creates.

Frequently asked questions

Why does outputs.releases_created behave differently compared to v3?
Since v4 the action always sets releases_created, to the string true or false, and a non-empty string is truthy in a GitHub expression. A bare if: ${{ steps.release.outputs.releases_created }} therefore always passes. Write == 'true' instead; it also works for release_created, which is only set when a release exists.
Is there an example of release-please with a monorepo?
Yes: omit release-type, list each package path under packages in release-please-config.json, and record each current version in .release-please-manifest.json. Turn on include-component-in-tag so tags do not collide, and read per-package outputs as outputs['<path>--release_created'].
How do I use release-please-action to perform a hotfix?
Release the older line from its own branch. The README's pattern is to trigger the workflow on pushes to a maintenance branch such as 1.x as well as main, and pass target-branch: ${{ github.ref_name }}, so a fix: merged into 1.x gets its own release PR against that branch and its own patch tag.
How do I attach build artifacts to the release release-please creates?
Gate an upload step on release_created == 'true' and push files to the tag it outputs. The README uses gh release upload with the tag_name output; softprops/action-gh-release with tag_name set works too, because it updates the existing release instead of creating one.

References

release-please runs on every push to main. Latchkey bills it at $0.0025/min at 2 vCPU. Start free → 30-day trial · No credit card