Skip to content

ci: release with release-please - #78

Closed
dduugg wants to merge 7 commits into
mainfrom
automate-releases
Closed

dduugg wants to merge 7 commits into
mainfrom
automate-releases

Conversation

@dduugg

@dduugg dduugg commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23. Thanks to @technicalpickles for the groundwork in #20 and #22, and for the write-up in #23.

Summary

Releases are automated with release-please, following Gusto/rubocop-gusto, and still publish with RubyGems trusted publishing (OIDC) through rubygems/release-gem.

  1. Each push to main runs release-please, which keeps a release PR open. The PR bumps lib/singed/version.rb, the singed (x.y.z) line in Gemfile.lock, and CHANGELOG.md, based on the Conventional Commits merged since the last release.
  2. Merging the release PR makes release-please create the vX.Y.Z tag and the GitHub release, with the changelog as its notes.
  3. In the same workflow run, the release job publishes the gem to RubyGems.org.

Contributors now need Conventional Commit PR titles, because the squash-merged title decides the version:

  • feat: bumps the minor version.
  • fix:, perf: and revert: bump the patch version.
  • chore:, ci: and docs: don't release anything.

A new check flags titles that don't follow the format; it isn't a required check. The README and AGENTS.md describe the process.

How it differs from rubocop-gusto

  • Publishing runs in the same workflow run, when release-please reports release_created, rather than in a separate workflow on release: published. A release created with GITHUB_TOKEN doesn't trigger other workflows, and rubocop-gusto works around that with a GitHub App token. rubyatscale has no release App, so this avoids needing one.
  • CI on the release PR is dispatched.
    • release-please opens and updates its PR with GITHUB_TOKEN, which doesn't trigger workflows. Since Sorbet is a required check, the release PR could never be merged.
    • The release-pr-checks job therefore runs gh workflow run for build.yml, rubocop.yml and sorbet.yml on the release branch whenever release-please opens or updates the PR. GITHUB_TOKEN can trigger workflow_dispatch, and the dispatched runs report on the PR's head commit. Those three workflows gain a workflow_dispatch trigger.
    • If another workflow later becomes a required check, it has to be added to that list.
  • No separate lockfile job. release-please's Ruby strategy updates the gem's own Gemfile.lock line when package-name is set; its updater skips the lockfile when the name is empty. rubocop-gusto doesn't set a package name, which is why it runs bundle install on its release PR.
  • bump-minor-pre-major is on, so a breaking change before 1.0 bumps the minor version instead of jumping to 1.0.0.
  • GitHub-hosted runners, since Gusto's runner group isn't available to rubyatscale.

Other changes

  • The version moves from a literal in singed.gemspec to lib/singed/version.rb (Singed::VERSION), which is the file release-please's Ruby strategy updates. lib/singed.rb requires it, so apps that install the gem get Singed::VERSION too. The built gem is unchanged at 0.3.0.
  • The release-please job has issues: write. The autorelease: pending and autorelease: tagged labels don't exist yet, and GITHUB_TOKEN can't create them with pull-requests: write alone (googleapis/release-please-action#1105).
  • Dependabot: its commits get a chore(deps) prefix, so its PRs pass the title check. It now updates GitHub Actions as well as the bundle, with a 7-day cooldown.
  • push_gem.yml keeps its filename and the rubygems.org environment, because the trusted publisher on RubyGems.org is registered against them.
  • The publish job:
    • checks out the release tag, so it builds exactly what was tagged even if main has moved;
    • needs only contents: read. release-gem fetches tags before rake release, and Bundler's release task skips pushing the branch and tag when the tag already exists;
    • runs a frozen bundle install without a cache;
    • posts to Slack on failure.
  • Retrying a failed publish: re-run the failed jobs of that workflow run. The release_created and tag_name outputs carry over, and the tag checkout means this still works after later merges.
  • Action upgrades:
    • release-gem goes from v1.1.0 to v1.4.1, which fetches tags before releasing and keeps the push token in git's credential cache instead of on disk.
    • harden-runner goes from v2.11.0, which has open advisories, to v2.21.1.
    • checkout and setup-ruby now match the pins in the other workflows.
  • No CHANGELOG.md yet. release-please creates it with the first release. A file holding only # Changelog would have left a stray ## Changelog heading below the first entry.

First release

None of the commits since v0.3.0 follow Conventional Commits, so release-please can't see the features or the Ruby floor change that have merged since then. The block below replaces this PR's squash commit for release-please. The release PR it produces should propose 0.4.0 with these notes, and it will open as soon as this PR merges.

BEGIN_COMMIT_OVERRIDE
feat!: require Ruby 3.3 or later (#67)
feat: add Singed.start and Singed.stop to record a flamegraph without a block (#52)
feat: add a Sidekiq server middleware (#53)
feat: add a sample rate option to the rbspy integration (#47)
feat: bundle speedscope with the gem, falling back to npx (#56)
feat: add inline RBS type signatures (#77)
feat: define Singed::VERSION
fix: require pathname in lib/singed.rb (#49)
ci: release with release-please
END_COMMIT_OVERRIDE

Before the first release

  • Check the trusted publisher: a trusted publisher should be registered on RubyGems.org for repository rubyatscale/singed, workflow push_gem.yml, environment rubygems.org. It's configured at https://rubygems.org/gems/singed/trusted_publishers; the gem's owners are techpickles and gusto-open-source.
    • This workflow has never run, so the publisher is unverified. If it's missing, the publish job fails at the credentials step and nothing is published.
    • The tag and GitHub release already exist by that point, so after fixing it, re-run the failed job.
  • Change the default squash-merge commit message to "Pull request title" (Settings → General → Pull Requests). It's currently "Default message".
    • With "Default message", a single-commit PR squashes with its commit's title rather than the PR title. For example, Add explicit GITHUB_TOKEN permissions to CI Test workflow #74 did.
    • A PR titled feat: … can therefore land as a commit release-please can't parse, and release nothing.
    • "Pull request title" also leaves the squash body empty, so a BREAKING CHANGE: line in some commit's body can't change the version unnoticed.
  • Remove the old secret: the repo secret GEM_HOST_API_KEY isn't used and can be deleted once a release has gone through.

Test plan

  • actionlint is clean on the changed workflows. zizmor (--persona pedantic, online audits) is clean on push_gem.yml, validate-pr-title.yml and dependabot.yml. build.yml has an unrelated existing finding.
  • Dry run of release-please 17 against this branch (release-pr --dry-run): the config and manifest parse, and it finds the v0.3.0 release at c2792b3. It opens no release PR, because none of the commits since then parse as user-facing.
  • Ran release-please 17.6.0's parser, versioning and changelog code on the override block. It gives 0.4.0, with a breaking-changes section, seven features and one fix.
  • Checked release-please's source:
    • the Ruby version-file updater rewrites the version string in lib/singed/version.rb;
    • the lockfile updater rewrites singed (0.3.0) when package-name is set;
    • the changelog is created if it's missing;
    • prs_created and pr are set when it opens or updates the release PR, and not when the PR is unchanged.
  • Checked that release-gem v1.4.1 runs git fetch --tags --force before rake release, and that Bundler's release task skips git push when the tag already exists.
  • rspec (42 examples), rubocop and srb tc pass. gem build still produces singed 0.3.0 with lib/singed/version.rb included.
  • CI passes, including the new PR title check on this PR.
  • After merge, the 0.4.0 release PR opens and gets CI Test, RuboCop and Sorbet runs from the dispatch, and merging it publishes.

Fixes #23. push_gem.yml already published through trusted publishing with
rubygems/release-gem, but only by hand, and it had never run. It now runs
after CI Test passes on main, and publishes only when singed.gemspec's
version isn't on RubyGems yet, so bumping the version is the release, as
with the rubyatscale gems that use shared-config's cd.yml. It still works
by hand for retries, and skips a version that's already published.

- The check job only lets a push to this repo through, since a fork's pull
  request can come from a branch named main.
- `rake release` pushes the current branch along with the tag, so the
  release job checks out main itself, rather than a detached HEAD, and first
  confirms main is still the commit CI tested.
- The release job runs `bundle install` without a cache, since it publishes.
- Bumps rubygems/release-gem from v1.1.0 to v1.4.1, which fetches tags
  before releasing and keeps credentials off disk.
- Creates the GitHub release, and posts to Slack on failure, like cd.yml.

The workflow's filename and the rubygems.org environment stay as they are,
since the trusted publisher on RubyGems.org is registered against them.
If `gh release create` failed after the gem was already on RubyGems, a
retry skipped everything, since the version was published, so the GitHub
release never got created. The check job now also looks for a GitHub
release for the version, and a separate job creates it whenever it's
missing, whether the gem was just published or already had been.
rubygems/release-gem v1.4.1 stores the token in git's credential cache for the tag push and clears it afterwards, so the checkout doesn't need to leave one behind.
@dduugg
dduugg requested a review from a team as a code owner September 29, 2026 05:20
@github-project-automation github-project-automation Bot moved this to Triage in Modularity Sep 29, 2026
Comment thread .github/workflows/push_gem.yml Fixed
CodeQL flagged actions/cache-poisoning/poisonable-step: the check job
checked out the workflow_run head SHA and then ran code from it, since
Gem::Specification.load evaluates the gemspec. The job's `if` already
limits it to pushes to this repo, but the analysis can't see that. The job
now fetches singed.gemspec at that SHA through the API and reads
spec.version with a strict pattern, so nothing from the commit runs and the
job no longer needs Ruby. If the gemspec ever stops using a string literal
for the version, the job fails with an error instead of guessing.
Follows Gusto/rubocop-gusto: release-please keeps a release PR open that
bumps the version and CHANGELOG.md from Conventional Commits, and merging it
tags the release and creates the GitHub release. The gem is still published
with trusted publishing through rubygems/release-gem.

- Publishing runs in the same workflow run, when release-please reports
  release_created. rubocop-gusto publishes on `release: published` with a
  GitHub App token, because a release created with GITHUB_TOKEN doesn't
  trigger other workflows; rubyatscale has no such app. push_gem.yml keeps
  its filename and the rubygems.org environment, which the trusted publisher
  is registered against.
- The version moves from a literal in the gemspec to lib/singed/version.rb,
  since release-please's Ruby strategy updates a VERSION constant there. It
  updates singed's own version line in Gemfile.lock too, so there's no
  separate lockfile job.
- bump-minor-pre-major, so a breaking change before 1.0 bumps the minor
  version rather than jumping to 1.0.0.
- A PR title check enforces Conventional Commits, since the squash-merged
  title drives the version, and Dependabot's commits get a chore(deps)
  prefix so its PRs pass it.
- README and AGENTS.md describe the process.

The version-bump check job from the previous approach is gone.
@dduugg dduugg changed the title Release automatically when the gemspec version changes ci: release with release-please Sep 29, 2026
AGENTS.md conflicted, since #77 added a Types section where this branch
added a Pull requests section; both are kept. #77 also makes everything
under lib/ `# typed: strict`, so lib/singed/version.rb gets the sigil too.
A review turned up several problems with the release-please setup:

- Release PRs couldn't be merged. Sorbet is a required check, and
  release-please pushes its PR with GITHUB_TOKEN, which doesn't trigger
  workflows. A new job dispatches the CI workflows on the release branch
  whenever release-please opens or updates the PR, since GITHUB_TOKEN can
  trigger workflow_dispatch. build.yml, rubocop.yml and sorbet.yml gain
  that trigger.
- The release-please job gets issues: write. The autorelease labels don't
  exist yet, and GITHUB_TOKEN can't create them with pull-requests: write
  alone (googleapis/release-please-action#1105).
- The publish job checks out the release tag instead of main, and the
  check that main hadn't moved is gone. release-gem fetches tags before
  `rake release`, which then skips pushing the branch and tag, so the
  branch checkout wasn't needed. The check also made "re-run failed jobs"
  fail for good once anything merged, and it compared against github.sha
  rather than the tagged commit. With nothing to push, the job only needs
  contents: read. bundle install runs frozen.
- CHANGELOG.md is removed. The Changelog updater found no version header
  in a file holding only "# Changelog", so it would have left a stray
  "## Changelog" heading below the first entry. release-please creates the
  file when it's missing.
- harden-runner goes from v2.11.0, which has open advisories, to v2.21.1.
  checkout and setup-ruby match the pins in the other workflows, and
  Dependabot now updates GitHub Actions too, with a 7-day cooldown.
- The PR title regex rejects nested parentheses in the scope, which
  release-please can't parse. The error message and README say that perf
  and revert release a patch, and that a BREAKING CHANGE footer counts as
  breaking. The README no longer claims the title check enforces anything,
  since it isn't required.
- lib/singed.rb requires singed/version, so Singed::VERSION is defined for
  apps that install the gem, not only for those loading it by path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Automate releasing

2 participants