Skip to content

Comments

Push with auto-attestation#9325

Open
hsbt wants to merge 7 commits intomasterfrom
push-with-attestation
Open

Push with auto-attestation#9325
hsbt wants to merge 7 commits intomasterfrom
push-with-attestation

Conversation

@hsbt
Copy link
Member

@hsbt hsbt commented Feb 10, 2026

What was the end-user or developer problem that led to this PR?

We should expand to use attestation of rubygems. The current toolchain only enabled attestation by https://github.com/rubygems/release-gem or --attestation option with local sigstore json file.

What is your fix for the problem, implemented in this PR?

Integrate rubygems-attestation-patch.rb to gem push command. After that, gem push command perfome:

  • Enable attestation with sigstore-cli and that result.
  • When --attestation option provided, gem push only uses that.
  • When sigstore-cli failed, gem push perform without atttestation
  • Disable auto-attestation with JRuby or not rubygems.org host

TODO: After merging this, we should avoid to load rubygems-attestation-patch.rb at rubygems/release-gem with RubyGems >= 4.1.

Make sure the following tasks are checked

Copilot AI review requested due to automatic review settings February 10, 2026 10:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds automatic Sigstore attestation support to gem push (targeting rubygems.org), generating an attestation bundle via sigstore-cli when no --attestation is provided and falling back to a non-attested push if attestation fails.

Changes:

  • Updated Gem::Commands::PushCommand to push with multipart attestations by default (rubygems.org-only), and retry without attestations on errors.
  • Added attest! implementation using gem exec sigstore-cli:0.2.2 sign ... --bundle ....
  • Refactored push command tests to validate multipart attestation requests via a shared helper and added tests for auto-attestation and fallback behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
lib/rubygems/commands/push_command.rb Implements auto-attestation flow, multipart request building, and fallback-to-non-attested push logic.
test/rubygems/test_gem_commands_push_command.rb Adds/updates tests for multipart attestation requests, auto-attestation behavior, and fallback behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -92,23 +92,64 @@ def send_gem(name)
private

def send_push_request(name, args)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early return for RUBY_ENGINE == "jruby" || !attestation_supported_host? skips all attestation, including when the user explicitly passes --attestation. That changes existing behavior and also contradicts the PR description (“When --attestation option provided, gem push only uses that.”). Consider only disabling auto attestation here (i.e., still call send_push_request_with_attestation when options[:attestations].any?), and gate the attest! path separately for JRuby/unsupported hosts.

Suggested change
def send_push_request(name, args)
def send_push_request(name, args)
if options[:attestations].any?
begin
return send_push_request_with_attestation(name, args)
rescue StandardError => e
alert_warning "Failed to push with attestation, retrying without attestation.\n#{e.full_message}"
return send_push_request_without_attestation(name, args)
end
end

Copilot uses AI. Check for mistakes.
begin
send_push_request_with_attestation(name, args)
rescue StandardError => e
alert_warning "Failed to push with attestation, retrying without attestation.\n#{e.full_message}"
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alert_warning includes e.full_message, which typically contains a full backtrace and can be extremely verbose/noisy (and may leak local paths). Consider logging only e.message by default and printing the full backtrace only under Gem.configuration.really_verbose (or similar), while still retrying without attestation.

Suggested change
alert_warning "Failed to push with attestation, retrying without attestation.\n#{e.full_message}"
message = "Failed to push with attestation, retrying without attestation."
if Gem.configuration.really_verbose
message = "#{message}\n#{e.full_message}"
else
message = "#{message}\n#{e.message}"
end
alert_warning message

Copilot uses AI. Check for mistakes.
Comment on lines 140 to 151
bundle = "#{name}.sigstore.json"
env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h
out, st = Open3.capture2e(
env,
Gem.ruby, "-S", "gem", "exec",
"sigstore-cli:0.2.2", "sign", name, "--bundle", bundle,
unsetenv_others: true
)
raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success?

bundle
end
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attest! writes the bundle to a deterministic path next to the gem file (#{name}.sigstore.json) and never cleans it up. This can overwrite an existing file and leaves artifacts behind after a successful push. Consider using a Tempfile/tmpdir for the bundle (or at least avoid overwriting existing files) and deleting it after the multipart request is constructed/sent.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +166 to 168
def attestation_supported_host?
(@host || Gem.host) == "https://rubygems.org"
end
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attestation_supported_host? uses exact string equality against "https://rubygems.org". If the host is configured as https://rubygems.org/ (trailing slash) or otherwise equivalent, auto-attestation will be incorrectly disabled. Consider normalizing/parsing the URI (scheme + host) or comparing against Gem::DEFAULT_HOST after normalization.

Copilot uses AI. Check for mistakes.
Comment on lines 121 to 149
def test_execute_attestation_auto
@response = "Successfully registered gem: freewill (1.0.0)"
@fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK")

p1 = parts.shift
p2 = parts.shift
assert_equal "\r\n", parts.shift
assert_empty parts
attestation_path = "#{@path}.sigstore.json"
File.write(attestation_path, "auto-attestation")
@cmd.options[:args] = [@path]

assert_equal [
"Content-Disposition: form-data; name=\"gem\"; filename=\"#{@path}\"",
"Content-Type: application/octet-stream",
nil,
Gem.read_binary(@path),
].join("\r\n").b, p1
assert_equal [
"Content-Disposition: form-data; name=\"attestations\"",
nil,
"[#{Gem.read_binary("#{@path}.sigstore.json")}]",
].join("\r\n").b, p2
@cmd.stub(:attest!, attestation_path) do
@cmd.execute
end

assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class
content_length = @fetcher.last_request["Content-Length"].to_i
assert_equal content_length, @fetcher.last_request.body.length
assert_attestation_multipart Gem.read_binary(attestation_path)
end

def test_execute_attestation_fallback
@response = "Successfully registered gem: freewill (1.0.0)"
@fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK")

@cmd.options[:args] = [@path]

@cmd.stub(:attest!, proc { raise Gem::Exception, "boom" }) do
use_ui @ui do
@cmd.execute
end
end
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior adds branching for skipping auto-attestation on JRuby and on non-rubygems.org hosts, but the tests added here only cover the auto-attestation happy path and the fallback-on-error path. Add coverage that verifies: (1) on a non-rubygems.org host, attest! is not called and the request is a raw application/octet-stream push, and (2) on JRuby (can be simulated by stubbing RUBY_ENGINE access), auto-attestation is skipped.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Copy link
Contributor

Copilot AI commented Feb 10, 2026

@hsbt I've opened a new pull request, #9326, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Feb 10, 2026

@hsbt I've opened a new pull request, #9327, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 4 commits February 10, 2026 19:32
Co-authored-by: hsbt <12301+hsbt@users.noreply.github.com>
Co-authored-by: hsbt <12301+hsbt@users.noreply.github.com>
Co-authored-by: hsbt <12301+hsbt@users.noreply.github.com>
@hsbt hsbt force-pushed the push-with-attestation branch from 9f87b0f to da9e8e4 Compare February 10, 2026 10:35
@colby-swandale
Copy link
Member

I'm not sure making Attestations the default path going forward would be a net positive in its current form. I don't have the stats in front of me, but I believe the vast majority of gem pushes to rubygems.org are still from local dev machines, so the vast majority of gem push will start showing “Failed to push with attestation, retrying without attestation” on every push, which could confuse users. And even when sigstore-cli is installed, it will still fail, since rubygems.org rejects anything not published via GitHub Actions (as of now).

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants