diff --git a/action.yml b/action.yml index 71988e2..d6e78e7 100644 --- a/action.yml +++ b/action.yml @@ -9,6 +9,13 @@ inputs: description: "Whether to setup the trusted publisher for the gem" required: false default: "true" + attestations: + description: >- + [EXPERIMENTAL] + Enable experimental support for sigstore attestations. + Only works with RubyGems.org via Trusted Publishing. + required: false + default: "true" outputs: {} branding: color: "red" @@ -29,6 +36,8 @@ runs: - name: Run release rake task run: bundle exec rake release shell: bash + env: + RUBYOPT: "${{ inputs.attestations == 'true' && format('-r{0}/rubygems-attestation-patch.rb {1}', github.action_path, env.RUBYOPT) || env.RUBYOPT }}" - name: Wait for release to propagate if: ${{ inputs.await-release == 'true' }} run: gem exec rubygems-await pkg/*.gem diff --git a/rubygems-attestation-patch.rb b/rubygems-attestation-patch.rb new file mode 100644 index 0000000..e3f645e --- /dev/null +++ b/rubygems-attestation-patch.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +return unless defined?(Gem) + +require "rubygems/commands/push_command" + +Gem::Commands::PushCommand.prepend(Module.new do + def send_push_request(name, args) + return super if options[:attestations]&.any? || @host != "https://rubygems.org" + + 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}" + super + end + end + + def send_push_request_with_attestation(name, args) + attestation = attest!(name) + if options[:attestations] + options[:attestations] << attestation + send_push_request(name, args) + else + rubygems_api_request(*args, scope: get_push_scope) do |request| + request.set_form([ + ["gem", Gem.read_binary(name), { filename: name, content_type: "application/octet-stream" }], + ["attestations", "[#{Gem.read_binary(attestation)}]", { content_type: "application/json" }] + ], "multipart/form-data") + request.add_field "Authorization", api_key + end + end + end + + def attest!(name) + require "open3" + 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.1", "sign", name, "--bundle", bundle, + unsetenv_others: true + ) + raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success? + + bundle + end +end)