Releases are created via a GitOps workflow. Merging a release/vX.Y.Z branch
into main automatically tags the commit and publishes the GitHub Release.
The release branch needs at least one commit so GitHub will allow a PR to be opened. Use an empty commit as a lightweight marker:
git fetch upstream
git checkout -b release/v3.0.0 upstream/main
git commit --allow-empty -s -m "chore: prepare release v3.0.0"
git push origin release/v3.0.0The release does not need to contain the changes being released — those are
already on main. The PR is a trigger: when it merges, the workflow tags the
PR's merge_commit_sha (the exact commit that landed on main), which includes
all prior work on the branch.
Open a PR from release/v3.0.0 targeting the main branch. Write the release
notes directly in the PR description using the format from prior releases:
## New Features
...
## Bug Fixes
...
## Documentation
...
## Other Changes
...The PR description becomes the GitHub Release body verbatim, so write it in its final form.
Branch protection on main requires approval from at least 3 of the 4 owners
listed in OWNERS.md. Reviewers should verify:
- The target commit is correct
- The release notes are accurate and complete
- All CI checks pass
Merge the PR. The release workflow automatically:
- Extracts the version from the branch name (
release/v3.0.0→v3.0.0) - Creates and pushes the git tag
- Publishes the GitHub Release with the PR body as release notes
Tags containing -alpha, -beta, or -rc (e.g., v3.0.0-rc.1) are
automatically marked as pre-release on GitHub. Use the same branch naming
convention: release/v3.0.0-rc.1.
Three levels of local validation are available without triggering a real release:
1. Validate the GoReleaser config:
goreleaser check2. Validate workflow structure and job matching (dry run):
act pull_request \
-e .github/act/release-event.json \
-W .github/workflows/release.yml \
-n3. Run the workflow end-to-end with a fake token (Colima + cached actions required):
act pull_request \
-e .github/act/release-event.json \
-W .github/workflows/release.yml \
-s GITHUB_TOKEN=fake \
--pull=false \
--action-offline-mode \
--container-daemon-socket -This runs all steps up to and including version extraction (version=vX.Y.Z will
appear in the output). The git push step then fails with a permission error —
that is expected and confirms no tag was pushed. The mock event payload is at
.github/act/release-event.json.
After a release, update oras-www
to reflect the new version. See the CLAUDE.md in that repository for the
exact steps.