Release from GitHub Actions with trusted publishing, and package only tracked files - #142
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Recovery archives the tag without Git metadata, preventing the Git-based manifest from reproducing the published gem.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Moves gem releases to a hardened, tag-triggered GitHub Actions pipeline and restricts packages to Git-tracked files.
Changes:
- Adds verified publishing, attestation, and recovery workflows.
- Adds package and registry verification with tests.
- Replaces laptop releases with
rake bumpandrake tag.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
File summaries
| File | Description |
|---|---|
.github/workflows/release.yml |
Adds the automated release pipeline. |
.github/workflows/release-recovery.yml |
Adds failed-release recovery. |
upright.gemspec |
Packages only Git-tracked files. |
script/release/package_verification.rb |
Implements package validation. |
script/release/verify_package.rb |
Exposes package verification CLI. |
script/release/registry.rb |
Implements registry reconciliation. |
script/release/registry_check.rb |
Exposes registry check CLI. |
script/release/registry_confirm.rb |
Exposes publication confirmation CLI. |
script/release/registry_download.rb |
Exposes verified download CLI. |
test/release/package_verification_test.rb |
Tests package verification. |
test/release/registry_test.rb |
Tests registry state handling. |
test/packaging_test.rb |
Tests tracked-file packaging. |
Rakefile |
Adds version bump and tagging tasks. |
bin/release |
Removes laptop-based releases. |
RELEASING.md |
Documents release and recovery procedures. |
CONTRIBUTING.md |
Links contributors to release documentation. |
CHANGELOG.md |
Records the disclosure and release changes. |
Review details
- Files reviewed: 16/17 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # tagged-src/ while the recovery helpers (script/release) keep | ||
| # running from the revision this workflow was dispatched on. | ||
| mkdir tagged-src | ||
| git archive "refs/tags/v${VERSION}" | tar --extract --directory tagged-src |
There was a problem hiding this comment.
Confirmed and fixed in de8b28d. A strict build from a git archive extraction fails because the gemspec's git ls-files returns nothing there. The verify job now checks the tag out with git worktree add --detach tagged-src, which keeps the index while the recovery helpers still run from the dispatch revision. Verified locally that a worktree build of the same commit produces the same SHA-256 as a build from the checkout. RELEASING.md updated to match.
There was a problem hiding this comment.
🔵 Needs a closer look
The security-sensitive publishing pipeline depends on external environment protections and trusted-publisher configuration that have not yet been exercised.
Review details
- Files reviewed: 16/17 changed files
- Comments generated: 1
- Review effort level: Balanced
| # The gemspec's Dir globs are evaluated against the working tree, so | ||
| # an untracked file dropped into app/ or lib/ during the build would | ||
| # be packaged. Filtering the expectation through `git ls-files` | ||
| # (the tag's tracked content) makes that contamination a failure | ||
| # instead of a silently wider gem. |
de8b28d to
a168647
Compare
The gemspec listed files with a filesystem glob, so anything present under app, config, db, lib or public at build time was packaged, including the ignored config/credentials/*.key files that reached the public 0.2.0 and 0.3.0 gems. The file list now comes from `git ls-files`, filtered to the same directories, and test/packaging_test.rb checks that an ignored key file cannot enter the package.
bin/release built the gem from the working tree, pushed it with a personal RubyGems OTP, and committed and tagged afterwards, so a tag and its gem did not have to match. It is replaced by .github/workflows/release.yml, which follows the shape RubyGems recommends: a v* tag push runs one job in the reviewer-gated release-rubygems environment. The job checks that the tag matches Upright::VERSION and is on origin/main, then runs rubygems/release-gem, which builds the gem, pushes it with short-lived OIDC credentials and a sigstore attestation, and waits for rubygems.org to serve it. Finally it creates the GitHub Release with the gem attached. `rake tag` creates and pushes the tag after checking the tree is clean and main is up to date. RELEASING.md documents the process, recovery, and the one-time setup.
|
Rewritten. I surveyed how RubyGems, Rails, Shopify and others release and cut this to the recommended shape: one job, |
Replaces the laptop release flow with a tag-triggered GitHub Actions workflow in the shape RubyGems recommends, and makes the gemspec package only files tracked by git.
Why
bin/releasebuilt the gem from the working tree, pushed it to RubyGems with a personal OTP, and only then committed and tagged. A tag and its gem did not have to match, and they did not:v0.1.2declares 0.1.1 andv0.3.0declares 0.2.0. The gemspec listed files with a filesystem glob, so anything present underapp,config,db,liborpublicat build time was packaged. That put the ignoredconfig/credentials/development.keyandtest.keyinto the public 0.2.0 and 0.3.0 gems. Treat both keys as disclosed.What changes
Package only tracked files. The gemspec takes its file list from
git ls-files, filtered to the same directories as before.test/packaging_test.rbchecks that an ignoredconfig/credentials/test.keycannot enter the package.Release from CI.
.github/workflows/release.ymlruns on av*tag push, as one job in therelease-rubygemsenvironment, which requires a reviewer. The job:Upright::VERSIONand that the tagged commit is onorigin/main.rubygems/release-gem, pinned by SHA. It obtains a short-lived credential from RubyGems trusted publishing, runsrake release(which builds the gem and, because the tag already exists, skips tagging and pushing git), pushes the gem with a sigstore attestation, and waits for rubygems.org to serve it.rake tagcreates and pushes the tag after checking that the tree is clean, the branch ismain,HEADequalsorigin/main, and the tag does not exist.bin/releaseis removed.RELEASING.mddocuments the process, verification, recovery, and the one-time setup.CONTRIBUTING.mdpoints at it.This is the same workflow shape as Shopify's gems, jekyll, faraday, factory_bot and about 1,200 other repositories, and the one the RubyGems trusted publishing guide shows. An earlier revision of this PR carried a six-job pipeline with a registry reconciliation state machine, a recovery workflow and package verification scripts, about 1,950 lines. None of the projects surveyed do any of that, and RubyGems now builds reproducibly by default, so the pipeline was cut to this.
Verification
rake buildruns of the same commit produce the same SHA-256, and the package contains no key files.Before the first release through this workflow
The one-time setup in
RELEASING.mdhas not been done: therelease-rubygemsenvironment with a required reviewer and av*tag policy, tag rulesets, a read-only default workflow token, the RubyGems trusted publisher forrelease.ymlin that environment, and removal of long-lived API keys from the owner accounts. The trusted publisher should be added right before tagging 0.4.0.