DEV Community

Cover image for release-please-action: release PRs from Conventional Commits
Kay
Kay

Posted on Originally published at latchkey.dev

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.

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.

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.

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.

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
Enter fullscreen mode Exit fullscreen mode

v5.0.0 is a lightweight tag; the SHA is the commit it names, from git ls-remote https://github.com/googleapis/release-please-action refs/tags/v5.0.0, read 2026-11-12.

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.

Input Default What it does
token github.token Token for creating release PRs, tags and releases
release-type none Strategy such as node, python, go, rust, simple; unset means use the manifest config
path repository root Release a package in a subdirectory
target-branch detected Branch to open the release PR against
config-file release-please-config.json Path to the config file
manifest-file .release-please-manifest.json Path to the versions manifest
include-component-in-tag false Prefix tags and branches with the component name
skip-github-release false Only open PRs, do not tag
skip-github-pull-request false Only tag, do not open PRs
skip-labeling false Do not label the PR
versioning-strategy default How the next version is computed
release-as none Force the next version
fork false Open the PR from a fork; needs a user token, not GITHUB_TOKEN
repo-url, github-api-url, github-graphql-url, changelog-host, proxy-server this repository and server For GitHub Enterprise and proxies

Defaults for config-file and manifest-file are from the README; the rest from action.yml at tag v5.0.0, read 2026-11-12.

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.

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

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
{
  "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" }
Enter fullscreen mode Exit fullscreen mode

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.

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 }}
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

v4 moved configuration out of the workflow

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

Release Change
v4.0.0 TypeScript 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.0 skip-labeling input
v4.4.0 versioning-strategy and release-as inputs
v5.0.0 Node 24 runtime, release-please library 17.6.0

From CHANGELOG.md at tag v5.0.0, read 2026-11-12. The README's examples still show @v4.

FAQ

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

Top comments (0)