Skip to content

Release automatically when the gemspec version changes - #78

Open
dduugg wants to merge 4 commits into
mainfrom
automate-releases
Open

dduugg wants to merge 4 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

push_gem.yml already publishes through RubyGems trusted publishing (OIDC) with rubygems/release-gem, but only when run by hand, and it has never run. This makes it automatic, in the same way the rubyatscale gems on shared-config's cd.yml work: bumping the version in singed.gemspec is the release.

  • After CI Test passes on main, a check job compares singed.gemspec's version with RubyGems. If the version isn't there yet, the release job publishes it with trusted publishing, which tags v<version> and pushes the gem. A separate job then creates the GitHub release with generated notes.
  • Pushes that don't change the version do nothing.
  • It can still be dispatched by hand to retry a failed run. Each step skips what's already done: it won't re-publish a version that's already on RubyGems, but it still creates a missing GitHub release.
  • On failure it posts to Slack through the org's SLACK_WEBHOOK_URL, like cd.yml.

It keeps trusted publishing rather than switching to shared-config's cd.yml, which uses the org-wide RUBYGEMS_API_KEY. The workflow's filename and the rubygems.org environment are unchanged, because the trusted publisher on RubyGems.org is registered against them.

Details

  • Which runs can release: only a push to this repo gets past the check job. A fork's pull request can come from a branch named main, which workflow_run's branches filter would otherwise match. zizmor flags workflow_run in general, so it's suppressed inline with that reason.
  • Reading the version: the check job reads singed.gemspec at the tested commit through the API and pulls out spec.version with a strict pattern, rather than checking out the commit and evaluating the gemspec. That way no code from the triggering commit runs in a privileged context; CodeQL flagged the checkout as a cache-poisoning risk. If the gemspec ever stops using a string literal for the version, the job fails with an error rather than guessing.
  • Branch checkout: 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.
  • No bundler cache: the release job runs bundle install without one, since it publishes. zizmor flagged the cache as a poisoning risk.
  • Action upgrade: rubygems/release-gem goes from v1.1.0 to v1.4.1. That release fetches tags before releasing, and it keeps the push token in git's credential cache instead of on disk, which is why the checkout uses persist-credentials: false.
  • Automate releasing #23's earlier failures:
    • "There are files that need to be committed first": vendor/bundle, pkg/ and the vendored speedscope are all gitignored now. I ran rake build and then rake release:guard_clean locally on a clean checkout, and it passes.
    • "Committer identity unknown", and the 403 on push: release-gem sets the identity itself, and the job has contents: write.

Before the first release

  • Trusted publisher: check that one is registered on RubyGems.org for this workflow. The gem's owners are techpickles and gusto-open-source, and it's configured at https://rubygems.org/gems/singed/trusted_publishers. It should be repository rubyatscale/singed, workflow push_gem.yml, environment rubygems.org. Since this workflow has never run, it's unverified. If it's missing, the release job fails at the credentials step with "No trusted publisher configured", and nothing is published.
  • Stale secret: the repo secret GEM_HOST_API_KEY isn't used by trusted publishing and can be deleted once a release has gone through.

Test plan

  • actionlint and zizmor (--persona pedantic) are clean.
  • Checked the version and release lookups against RubyGems and GitHub. For 0.3.0, the gem and the v0.3.0 release both exist, so nothing runs. For an unpublished version, the gem gets published and the release created.
  • rake build then rake release:guard_clean passes on a clean checkout.
  • CI passes.
  • The next version bump publishes, tags and creates the release automatically.

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
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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

Automate releasing

2 participants