Class: Gem::Commands::PushCommand
| Relationships & Source Files | |
| Super Chains via Extension / Inclusion / Inheritance | |
|
Class Chain:
self,
::Gem::Command
|
|
|
Instance Chain:
|
|
| Inherits: |
Gem::Command
|
| Defined in: | lib/rubygems/commands/push_command.rb |
Constant Summary
::Gem::Command - Inherited
::Gem::GemcutterUtilities - Included
Class Attribute Summary
::Gem::Command - Inherited
| .build_args | Arguments used when building gems. |
| .build_args=, .extra_args, .extra_args= | |
Class Method Summary
- .new ⇒ PushCommand constructor
::Gem::Command - Inherited
| .add_common_option, | |
| .add_specific_extra_args | Add a list of extra arguments for the given command. |
| .common_options, | |
| .new | Initializes a generic gem command named |
| .specific_extra_args | Return an array of extra arguments for the command. |
| .specific_extra_args_hash | Accessor for the specific extra args hash (self initializing). |
Instance Attribute Summary
- #attestation_supported_host? ⇒ Boolean readonly private
::Gem::GemcutterUtilities - Included
| #host | The host to connect to either from the RUBYGEMS_HOST environment variable or from the user's configuration. |
| #host=, #scope=, #webauthn_enabled?, #default_host? | |
::Gem::LocalRemoteOptions - Included
| #both? | Is fetching of local and remote information enabled? |
| #local? | Is local fetching enabled? |
| #remote? | Is remote fetching enabled? |
::Gem::Command - Inherited
| #command | The name of the command. |
| #defaults | The default options for the command. |
| #deprecated?, | |
| #options | The options for the command. |
| #program_name | The name of the command for command-line invocation. |
| #summary | A short description of the command. |
::Gem::DefaultUserInteraction - Included
Instance Method Summary
- #execute
- #send_gem(name)
- #attest!(name) private
- #gem_name_selector_description private
- #get_hosts_for(name) private
- #get_push_scope private
- #load_attestation(file) private
- #multiple_matches_message(matches) private
- #resolve_gem_name(names) private
- #send_push_request(name, args) private
- #send_push_request_with_attestation(name, args) private
- #send_push_request_without_attestation(name, args) private
- #validate_attestation_json(data, source) private
- #arguments Internal use only
- #description Internal use only
- #usage Internal use only
::Gem::GemcutterUtilities - Included
| #add_key_option | Add the --key option. |
| #add_otp_option | Add the --otp option. |
| #api_key | The API key from the command options or from the user's configuration. |
| #mfa_unauthorized?, | |
| #otp | The OTP code from the command options or from the user's configuration. |
| #rubygems_api_request | |
| #set_api_key | Returns true when the user has enabled multifactor authentication from |
| #sign_in | Signs in with the RubyGems API at |
| #stored_api_key_named | The default key, when |
| #update_scope, | |
| #verify_api_key | Retrieves the pre-configured API key |
| #with_response | If |
| #api_key_forbidden?, #fetch_otp, #get_key_name, #get_mfa_params, #get_scope_params, #get_user_profile, #pretty_host, #request_with_otp, #wait_for_otp_thread, #webauthn_verification_url | |
::Gem::VersionOption - Included
| #add_platform_option | Add the --platform option to the option parser. |
| #add_prerelease_option | Add the --prerelease option to the option parser. |
| #add_ruby_abi_option | Add the --ruby-abi option to the option parser. |
| #add_version_option | Add the --version option to the option parser. |
| #get_platform_from_requirements | Extract platform given on the command line. |
::Gem::LocalRemoteOptions - Included
| #accept_uri_http | Allows |
| #add_bulk_threshold_option | Add the --bulk-threshold option. |
| #add_clear_sources_option | Add the --clear-sources option. |
| #add_local_remote_options | Add local/remote options to the command line parser. |
| #add_proxy_option | Add the --http-proxy option. |
| #add_source_option | Add the --source option. |
| #add_update_sources_option | Add the --update-sources option. |
::Gem::Command - Inherited
| #add_extra_args | Adds extra args from ~/.gemrc. |
| #add_option | Add a command-line option and handler to the command. |
| #arguments | Override to provide details of the arguments a command takes. |
| #begins? | True if |
| #check_deprecated_options, | |
| #defaults_str | Override to display the default values of the command options. |
| #deprecate_option | Mark a command-line option as deprecated, and optionally specify a deprecation horizon. |
| #description | Override to display a longer description of what this command does. |
| #execute | Override to provide command handling. |
| #get_all_gem_names | Get all gem names from the command line. |
| #get_all_gem_names_and_versions | Get all [gem, version] from the command line. |
| #get_one_gem_name | Get a single gem name from the command line. |
| #get_one_optional_argument | Get a single optional argument from the command line. |
| #handle_options | Handle the given list of arguments by parsing them and recording the results. |
| #handles? | True if the command handles the given argument list. |
| #invoke | Invoke the command with the given list of arguments. |
| #invoke_with_build_args | Invoke the command with the given list of normal arguments and additional build arguments. |
| #merge_options | Merge a set of command options with the set of default options (without modifying the default option hash). |
| #remove_option | Remove previously defined command-line argument |
| #show_help | Display the help message for the command. |
| #show_lookup_failure | Display to the user that a gem couldn't be found and reasons why --. |
| #usage | Override to display the usage for an individual gem command. |
| #when_invoked | Call the given block when invoked. |
| #add_parser_run_info | Adds a section with |
| #configure_options, | |
| #create_option_parser | Creates an option parser and fills it in with the help info for the command. |
| #option_is_deprecated?, | |
| #parser | Create on demand parser. |
| #wrap | Wraps |
| #extract_gem_name_and_version, #add_parser_description, #add_parser_options, #add_parser_summary | |
::Gem::UserInteraction - Included
| #alert | Displays an alert |
| #alert_error | Displays an error |
| #alert_warning | Displays a warning |
| #ask | Asks a |
| #ask_for_password | Asks for a password with a |
| #ask_yes_no | Asks a yes or no |
| #choose_from_list | Asks the user to answer |
| #say | Displays the given |
| #terminate_interaction | Terminates the RubyGems process with the given |
| #verbose | Calls |
::Gem::DefaultUserInteraction - Included
::Gem::Text - Included
| #clean_text | Remove any non-printable characters and make the text suitable for printing. |
| #format_text | Wraps |
| #levenshtein_distance | Returns a value representing the "cost" of transforming str1 into str2 Vendored version of |
| #truncate_text, #min3 | |
Constructor Details
.new ⇒ PushCommand
# File 'lib/rubygems/commands/push_command.rb', line 36
def initialize super "push", "Push a gem up to the gem server", host: host, attestations: [] @user_defined_host = false add_proxy_option add_key_option add_otp_option add_option("--host HOST", "Push to another gemcutter-compatible host", " (e.g. https://rubygems.org)") do |value, | [:host] = value @user_defined_host = true end add_option("--platform PLATFORM", "Push a gem for a specific platform", " (e.g. x86_64-darwin-20)") do |value, | [:platform] = value end add_ruby_abi_option("push", " (e.g. 3.4)") add_option("--attestation FILE", "Push with sigstore attestations", " (FILE must be a JSON sigstore bundle)") do |value, | [:attestations] << value end @host = nil end
Instance Attribute Details
#attestation_supported_host? ⇒ Boolean (readonly, private)
[ GitHub ]
Instance Method Details
#arguments
# File 'lib/rubygems/commands/push_command.rb', line 28
def arguments # :nodoc: "GEM built gem to push up" end
#attest!(name) (private)
[ GitHub ]# File 'lib/rubygems/commands/push_command.rb', line 231
def attest!(name) require "open3" require "shellwords" require "tempfile" env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h Tempfile.create([File.basename(name, ".*"), ".sigstore.json"]) do |tempfile| tempfile.close bundle = tempfile.path # Gem.ruby is quoted if it contains whitespace, so split it into argv # elements to keep the quotes out of the spawned command. out, st = Open3.capture2e( env, *Shellwords.split(Gem.ruby), "-S", "gem", "exec", "--conservative", "sigstore-cli", "sign", name, "--bundle", bundle, unsetenv_others: true ) raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success? validate_attestation_json(Gem.read_binary(bundle), "generated by sigstore-cli") end end
#description
# File 'lib/rubygems/commands/push_command.rb', line 14
def description # :nodoc: <<-EOF The push command uploads a gem to the push server (the default is https://rubygems.org) and adds it to the index. The gem can be removed from the index and deleted from the server using the yank command. For further discussion see the help for the yank command. The push command will use ~/.gem/credentials to authenticate to a server, but you can use the RubyGems environment variable GEM_HOST_API_KEY to set the api key to authenticate. If the :credential_store: gemrc option (or RUBYGEMS_CREDENTIAL_STORE environment variable) is set, the API key is stored in and read from the credential store it selects instead of ~/.gem/credentials. The API key to send is resolved in this order: the GEM_HOST_API_KEY environment variable, the --key option, the host's own key in the credential store (when :credential_store: is set), the host's own key in ~/.gem/credentials, then the default RubyGems.org key from either place. The first one found is used. EOF end
#execute
[ GitHub ]# File 'lib/rubygems/commands/push_command.rb', line 69
def execute gem_name = if [:platform] || [:ruby_abi] resolve_gem_name(get_all_gem_names) else get_one_gem_name end default_gem_server, push_host = get_hosts_for(gem_name) @host = if @user_defined_host [:host] elsif default_gem_server default_gem_server elsif push_host push_host else [:host] end sign_in @host, scope: get_push_scope send_gem(gem_name) end
#gem_name_selector_description (private)
[ GitHub ]#get_hosts_for(name) (private)
[ GitHub ]#get_push_scope (private)
[ GitHub ]# File 'lib/rubygems/commands/push_command.rb', line 265
def get_push_scope :push_rubygem end
#load_attestation(file) (private)
[ GitHub ]# File 'lib/rubygems/commands/push_command.rb', line 210
def load_attestation(file) data = begin Gem.read_binary(file) rescue SystemCallError, IOError, ArgumentError => e raise Gem::Exception, "Failed to read attestation #{file}: #{e.}" end validate_attestation_json(data, file) end
#multiple_matches_message(matches) (private)
[ GitHub ]# File 'lib/rubygems/commands/push_command.rb', line 141
def (matches) = "Multiple gems matched #{gem_name_selector_description}: #{matches.map(&:first).join(", ")}" if [:platform] && ![:ruby_abi] ruby_abis = matches.filter_map {|_, spec| spec.ruby_abi }.uniq.sort += "\nSpecify --ruby-abi with one of: #{ruby_abis.join(", ")}" unless ruby_abis.empty? += "\nTo push a gem without a Ruby ABI, pass the exact filename." if matches.any? {|_, spec| spec.ruby_abi.nil? } elsif [:ruby_abi] && ![:platform] platforms = matches.map {|_, spec| spec.platform.to_s }.uniq.sort += "\nSpecify --platform with one of: #{platforms.join(", ")}" unless platforms.empty? end end
#resolve_gem_name(names) (private)
# File 'lib/rubygems/commands/push_command.rb', line 112
def resolve_gem_name(names) platform = [:platform] && Gem::Platform.new([:platform]) ruby_abi = [:ruby_abi] candidates = names.filter_map do |name| [name, Gem::Package.new(name).spec] rescue Gem::Package::FormatError => e alert_warning "Skipping #{name}: #{e.}" nil end matches = candidates.select do |_, spec| (!platform || spec.platform == platform) && (!ruby_abi || (Gem::ContentAddress.eligible?(spec) && spec.ruby_abi == ruby_abi)) end raise Gem::CommandLineError, "No gem matched #{gem_name_selector_description}" if matches.empty? raise Gem::CommandLineError, (matches) if matches.length > 1 matches.first.first end
#send_gem(name)
[ GitHub ]# File 'lib/rubygems/commands/push_command.rb', line 93
def send_gem(name) args = [:post, "api/v1/gems"] _, push_host = get_hosts_for(name) @host ||= push_host # Always include @host, even if it's nil args += [@host, push_host] say "Pushing gem to #{@host || Gem.host}..." response = send_push_request(name, args) with_response response end
#send_push_request(name, args) (private)
[ GitHub ]# File 'lib/rubygems/commands/push_command.rb', line 156
def send_push_request(name, args) # Always honor explicit --attestation option # Auto-attestation is only supported on rubygems.org with GitHub Actions (not JRuby) if [:attestations].any? || (RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"] == "true") send_push_request_with_attestation(name, args) else send_push_request_without_attestation(name, args) end end
#send_push_request_with_attestation(name, args) (private)
[ GitHub ]# File 'lib/rubygems/commands/push_command.rb', line 177
def send_push_request_with_attestation(name, args) attestations = if [:attestations].any? [:attestations].map do |attestation| load_attestation(attestation) end else # Only the opportunistic signing step falls back. The request below stays # outside this rescue because once the server may have seen the attested # push, a network error must not trigger an unattested retry. begin [attest!(name)] rescue StandardError => e = "Failed to create an attestation, pushing without one.\n" += if Gem.configuration.really_verbose e. else e. end alert_warning return send_push_request_without_attestation(name, args) end end bundles = "[" + attestations.join(",") + "]" 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", bundles, { content_type: "application/json" }], ], "multipart/form-data") request.add_field "Authorization", api_key end end
#send_push_request_without_attestation(name, args) (private)
[ GitHub ]# File 'lib/rubygems/commands/push_command.rb', line 166
def send_push_request_without_attestation(name, args) scope = get_push_scope rubygems_api_request(*args, scope: scope) do |request| body = Gem.read_binary name request.body = body request.add_field "Content-Type", "application/octet-stream" request.add_field "Content-Length", request.body.size request.add_field "Authorization", api_key end end
#usage
# File 'lib/rubygems/commands/push_command.rb', line 32
def usage # :nodoc: "#{program_name} GEM" end
#validate_attestation_json(data, source) (private)
# File 'lib/rubygems/commands/push_command.rb', line 219
def validate_attestation_json(data, source) require "json" parsed = begin JSON.parse(data) rescue JSON::ParserError => e raise Gem::Exception, "Attestation #{source} is not valid JSON: #{e.}" end raise Gem::Exception, "Attestation #{source} is not a JSON object" unless parsed.is_a?(Hash) data end