release_ceremony
A strict, verified release ceremony for RubyGems projects hosted on GitHub.
release_ceremony provides two commands:
release_ceremony prepare VERSION— on therelease-vVERSIONbranch, updates the version file, moves the changelog's Unreleased notes under a dated release heading, rewrites the comparison links, and runs the verification commands. It never commits or pushes.release_ceremony publish VERSION— after the release pull request has merged, validates the merge commit, tags itvVERSION, pushes the tag, watches the tag-triggered GitHub Actions release workflow, and confirms the new version appears on RubyGems.
Both commands validate aggressively and fail loudly: every transformation is exact-match, file writes are transactional, and nothing is tagged or pushed until you type the confirmation phrase.
The ceremony
VERSION=1.2.3
git switch -c "release-v$VERSION"
release_ceremony prepare "$VERSION"
# inspect the changes, then commit and push the branch
# open the pull request and merge it
release_ceremony publish "$VERSION"
Publication itself happens in CI: the pushed tag triggers your release
workflow, which should publish to RubyGems (see
Setting up Trusted Publishing). publish
only tags, watches, and verifies.
Installation
Add the gem to your Gemfile:
group :build do
gem 'release_ceremony', require: false
end
What it assumes
- The gem's version lives in
lib/<gem_name>/version.rbas exactly one single-quotedVERSION = 'X.Y.Z'assignment. CHANGELOG.mdfollows Keep a Changelog: exactly one## [Unreleased]heading with notes, dated release headings, and reference-style comparison links at the bottom.- Versions are strict
X.Y.Zsemantic versions (no prereleases), taggedvX.Y.Z. - Release work happens on a branch named
release-vX.Y.Z, merged tomainon theoriginremote via exactly one pull request. - The repository is on GitHub, with the
ghCLI installed and authenticated, and has arelease.ymlworkflow triggered by pushing the tag.
The gem name and repository URL are read from the repository's gemspec
(metadata['source_code_uri'], falling back to homepage). Everything else
above is a convention that .release_ceremony.yml can override.
Setting up Trusted Publishing
publish never pushes to RubyGems itself; the tag-triggered workflow does,
ideally with Trusted Publishing
and the rubygems/release-gem
action, so that no API key exists to be stored or leaked. One-time setup:
- Existing gem: on rubygems.org, open the gem → Trusted publishers →
Create, and enter the repository owner and name, the workflow filename
(e.g.
release.yml), and the environment name the workflow declares (e.g.release). - New gem: nothing is created on RubyGems manually. Instead, register a pending trusted publisher (Profile → OIDC → Pending trusted publishers) with the same fields plus the gem's name. The first successful push from the workflow creates the gem, converts the pending publisher into a normal one, and makes you the owner. Pending publishers expire 12 hours after creation, so register on the day of the first release; if it lapses, just register it again.
- GitHub environment: if the workflow's publish job declares
environment: release, create that environment in the repository settings (Settings → Environments). Trusted Publishing only accepts tokens minted from the registered environment, and you can attach protection rules such as required reviewers to it.
RubyGems also ships a CLI that automates the RubyGems side:
gem exec configure_trusted_publisher.
Re-running publish
publish converges when reality matches expectations and stops loudly when
it doesn't, so after most failures you can simply run it again:
- Everything before the confirmation prompt is read-only validation; a failure there has changed nothing.
- Tagging is guarded: if the remote tag already exists at the merge commit,
tagging and pushing are skipped; if only a local tag exists, it is pushed
as-is. A tag that exists anywhere but points to the wrong commit aborts the
ceremony before the prompt —
publishnever moves or deletes a tag. - Watching the workflow and checking RubyGems are read-only, so a run that died mid-watch or hit RubyGems index lag re-attaches to the completed workflow run and re-verifies. Re-running after a fully successful publish succeeds again without side effects.
The exception is a failed release workflow. Re-running publish does not
retry it: the tag is already on the remote, so nothing re-pushes, no new
workflow is triggered, and watching the same failed run fails again. Re-run
the workflow first, then re-run publish to watch and verify it:
gh run rerun <run-id> --failed
release_ceremony publish "$VERSION"
Configuration
Create .release_ceremony.yml in the repository root to override any of the
defaults:
gem_name: my_gem # default: the gemspec's name
repo_url: https://github.com/acme/my_gem # default: the gemspec's source_code_uri or homepage
version_file: lib/my_gem/version.rb # default: lib/<gem_name>/version.rb
changelog_file: CHANGELOG.md # default: CHANGELOG.md
workflow_file: release.yml # default: release.yml
default_branch: main # default: main
remote: origin # default: origin
verification_commands: # default: the four commands below
- bundle install
- bundle exec rspec
- bundle exec rubocop
- bundle exec rake build
verification_commands are split shell-style into words and run directly
(no shell), in order, from the repository root. A project that manages its
toolchain with mise, for example, would use
mise exec -- bundle exec rspec.
Comparison with create_github_release
create_github_release
automates the same job — releasing a gem hosted on GitHub through a release
branch and pull request — but makes the opposite choice at almost every step:
release_ceremony |
create_github_release |
|
|---|---|---|
| Committing and pushing | You commit, push, and open the PR yourself | One command commits, tags, pushes, and opens the PR |
| Tag timing | Tags the merge commit after the PR merges, validating it first | Tags the release branch before merging, requiring a manual fast-forward merge |
| Changelog | Promotes your curated Keep a Changelog Unreleased notes |
Generates release notes from the commit list |
| Verification | Runs configurable verification commands during prepare |
None |
| Publishing | Tag-triggered CI publishes; publish watches the workflow and confirms the version on RubyGems |
Manual rake release:rubygem_push from your machine |
| Version | Explicit X.Y.Z only |
Computes major/minor/patch bumps, supports pre-releases |
| GitHub release object | Not created | Created |
Choose create_github_release if you want the release automated away in one
command. Choose release_ceremony if you want curated release notes, a
human inspecting and merging the release pull request, publication only from
CI, and verification at every step.
Development
mise install
bundle install
bundle exec rspec # tests
bundle exec rubocop # static code analysis