Skip to content

OpenHands/release-actions

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

release-actions

Centralized, reusable release automation built on release-please.

This repo is self-referential: it uses its own reusable workflows to release itself, so its release notes are generated by the very automation it ships.

What you get

  • Automatically generated GitHub release notes, grouped by change type, from conventional PR titles — with links back to PRs and contributor attribution.
  • Automatic SemVer tagging — the version is derived from the conventional PR titles since the last release (fix → patch, feat → minor, breaking → major).
  • Automatic PR labelling — every PR is labelled with its type (type: feat, …) and, once shipped, with the release that included it (released: X.Y.Z).
  • Automatic updates to version references in files such as version.txt, package.json, pyproject.toml, and any other file release-please is configured to track.

Adopting this in another repo

The App credentials are already set up org-wide — there is nothing for you to create. RELEASE_APP_ID and RELEASE_APP_PRIVATE_KEY are configured as organization secrets and are available to every repo in this org. The secrets: inherit line in release.yml below is all that's needed to pass them through. Do not add them as repo secrets, do not generate a new GitHub App, and do not flag them as a missing prerequisite — they exist and they work. The only per-repo setup is the steps listed here.

  1. Add the two caller workflows. Copy .github/workflows/release.yml and pr.yml from this repo, and change the local uses: ./... paths to reference this repo at @main:

    # .github/workflows/release.yml
    on:
      push:
        branches: [main, 'release/**']
    jobs:
      release-please:
        permissions: { contents: write, pull-requests: write }
        secrets: inherit   # passes the org-level RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY through — already set up org-wide
        uses: OpenHands/release-actions/.github/workflows/release-please.yml@main
    # .github/workflows/pr.yml
    on:
      pull_request_target:        # runs on fork PRs too — see the note below
        types: [opened, edited, reopened, synchronize]
    jobs:
      pr-title:
        permissions: { pull-requests: write }
        # no `secrets: inherit` — the title workflow runs on GITHUB_TOKEN alone
        uses: OpenHands/release-actions/.github/workflows/pr-title.yml@main

    Both reference @main, so consumers pick up changes as soon as they land here.

    Why pull_request_target for pr.yml? It lets the title lint/label run on PRs from forks (external / non-member contributors), where a plain pull_request only gets a read-only token and the labeling step would fail. It's safe because the title workflow never checks out or runs PR code, reads the title only as event data (no ${{ }} interpolation), and runs on GITHUB_TOKEN alone — never the release App token. Don't add a checkout step or secrets: inherit to it.

  2. Add the four state files. Copy .github/release.yml, release-please-config.json, .release-please-manifest.json (seed { ".": "0.1.0" }), and version.txt (0.1.0).

  3. Apply the one-time repo settings (these can't be imported):

    # Squash-merge so the PR title becomes the commit release-please reads.
    gh api -X PATCH repos/<owner>/<repo> \
      -f squash_merge_commit_title=PR_TITLE -f squash_merge_commit_message=COMMIT_MESSAGES \
      -F allow_squash_merge=true -F allow_merge_commit=false -F allow_rebase_merge=false \
      -F delete_branch_on_merge=true

That's it — the next push to main opens a release PR.

Releasing more than one artifact from one repo

A repo can run several independent release lines — e.g. a public app and a separate cloud image — by calling release-please.yml once per line, each with its own config + manifest. release-please always feeds the root package (.) every commit, so each line decides its own scope from there:

  • give every line its own release-please-config.<name>.json + .release-please-manifest.<name>.json, and seed each manifest with that line's last released version;
  • point each call at its files with the config-file / manifest-file inputs. They default to release-please-config.json / .release-please-manifest.json, so the primary line needs no inputs;
  • set the tag shape per line (component, tag-separator, include-component-in-tag, include-v-in-tag) so the tags can't collide;
  • narrow a line with exclude-paths: [...] — a commit whose files are all under an excluded path is dropped from that line. Use changelog-type: default (not github) so notes are built from each line's own filtered commits rather than from a flat tag-to-tag diff.
# .github/workflows/release.yml
on: { push: { branches: [main, 'release/**'] } }
jobs:
  app:                       # tags X.Y.Z — uses the default config/manifest
    permissions: { contents: write, pull-requests: write }
    secrets: inherit
    uses: OpenHands/release-actions/.github/workflows/release-please.yml@main
  cloud:                     # tags cloud-X.Y.Z — its own config/manifest
    permissions: { contents: write, pull-requests: write }
    secrets: inherit
    uses: OpenHands/release-actions/.github/workflows/release-please.yml@main
    with:
      config-file: release-please-config.cloud.json
      manifest-file: .release-please-manifest.cloud.json

Each line opens and merges its own release PR independently, and the released: <tag> back-label is keyed on the tag, so the lines never clash.

Release flow

1. Open a PR against main

To get your work into a release, it must first be merged to main.

Create a PR and give it a Conventional Commit formatted titlefeat: …, fix: …, docs: …, etc.

When you open the PR, GitHub Actions will:

  1. Lint the title, to make sure it follows the Conventional Commit format.
  2. Label the PR with its type (type: feat, type: fix, …).

Once your PR is approved, it is squash-merged to main.

2. release-please processing

On merge, release-please runs on main and automatically updates a dedicated chore(main): release X.Y.Z PR. It aggregates the changelog (which eventually becomes the GitHub release notes) into the PR description, and it automatically determines what SemVer to use based on the unreleased commits on main. fix PRs become patch version bumps, feat PRs become minor version bumps, and breaking changes are major version bumps.

3. Creating a release

Merging the release PR is what publishes the release. Once it's merged:

  1. The merge commit is automatically tagged with its version (vX.Y.Z).
  2. Release notes are published to GitHub.
  3. GitHub Actions that publish release artifacts are executed.
  4. A GitHub Action runs that tags all the PRs included in a release with a released: X.Y.Z.

Hotfixing a shipped version

Releases aren't isolated from main: merging the chore(main): release … PR ships everything queued on main. So when, say, v0.2.0 is in production and you need a patch with only a fix, not whatever else has piled up on main since, you create a release/** branch off the release tag instead of shipping from main.

The release workflow also runs on release/**, so release-please drives the patch on a maintenance branch exactly like it does on main.

A typical hotfix flow would look like this:

  1. Fix main as usual. Open and squash-merge a normal fix: PR.
  2. Cut a maintenance branch from the tag
    git branch release/0.2 v0.2.0
    git push origin release/0.2
  3. Cherry-pick the fix onto the release line, as a PR:
    git checkout -b jl/backport-version-fix origin/release/0.2
    git cherry-pick <sha-from-main>
    git push -u origin jl/backport-version-fix
    gh pr create --base release/0.2 --title "fix: <conventional title>"
    Squash-merge it.
  4. Publish the patch. release-please opens a chore(release/0.2): release 0.2.1 PR on the branch. Merge it to publish the release.

Freezing a release to soak

Releasing straight from main — the Release flow above — is the default, and it's what we want for almost every release. A freeze is the exception: reach for it only when you genuinely need to soak a release in isolation, without main's newer work coming along.

Sometimes you want to validate a release for a while — a "soak" — before it ships, but main keeps collecting work you don't want in this release. Cutting a release branch ahead of time freezes the release: from the moment you branch, main can keep moving and none of it lands in what you're soaking.

release-please runs on release/**, so it builds the release PR on the frozen branch from exactly the commits you captured, just like it does on main. One thing to watch: because release-please is still running on main, main keeps its own chore(main): release … PR open for the same version while you soak. Leave it alone — merging it would publish straight from main and pull in everything that landed after your cut. You publish from the branch instead, then merge back.

A typical freeze would look like this:

  1. Cut the release branch from main when you're ready to freeze.
    git branch release/0.3 main
    git push origin release/0.3
    release-please opens a chore(release/0.3): release 0.3.0 PR on the branch. From here, main is free to move without touching it.
  2. Soak the branch — deploy it, validate it, take your time. main stays open for everyone else. If you find a problem, fix it exactly like a hotfix: land the fix on main, then cherry-pick it onto the branch as a PR (see Hotfixing a shipped version).
  3. Publish the release. When the soak passes, merge the branch's release PR. That tags v0.3.0 and publishes the GitHub release, same as a release cut from main.
  4. Merge the branch back into main. Open a PR merging release/0.3 into main and squash-merge it. This advances main to 0.3.0, so the next feat targets 0.4.0, and it clears the shadow release PR that was open on main.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors