Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/avo/resources/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ def fields # rubocop:disable Metrics
field :slug, as: :text, hide_on: :index
field :number, as: :text
field :platform, as: :text
field :ruby_abi, as: :text, title: "Ruby ABI"
field :content_address, as: :text

field :canonical_number, as: :text

Expand Down
4 changes: 2 additions & 2 deletions app/controllers/api/v2/contents_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def index
protected

def find_version
version_params = params.permit(:version_number, :platform)
@version = @rubygem.find_public_version(version_params[:version_number], version_params[:platform])
version_params = params.permit(:version_number, :platform, :ruby_abi)
@version = @rubygem.find_public_version(version_params[:version_number], version_params[:platform], version_params[:ruby_abi].presence)
render plain: "This version could not be found.", status: :not_found unless @version
end

Expand Down
4 changes: 2 additions & 2 deletions app/controllers/api/v2/versions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def show
cache_expiry_headers
set_surrogate_key "gem/#{@rubygem.name}"

version = @rubygem.public_version_payload(version_params[:number], version_params[:platform])
version = @rubygem.public_version_payload(version_params[:number], version_params[:platform], version_params[:ruby_abi].presence)
if version
respond_to do |format|
format.json { render json: version }
Expand All @@ -22,6 +22,6 @@ def show
protected

def version_params
params.permit(:platform, :number)
params.permit(:platform, :number, :ruby_abi)
end
end
1 change: 1 addition & 0 deletions app/models/events/rubygem_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Events::RubygemEvent < ApplicationRecord
VERSION_PUSHED = define_event "rubygem:version:pushed" do
attribute :number, :string
attribute :platform, :string
attribute :ruby_abi, :string
attribute :sha256, :string

attribute :pushed_by, :string
Expand Down
9 changes: 5 additions & 4 deletions app/models/rubygem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,16 @@ def public_versions_with_extra_version(extra_version)

# NB: this intentionally does not default the platform to ruby.
# Without platform, finds the most recent version by (position, created_at) ignoring platform.
def find_public_version(number, platform = nil)
def find_public_version(number, platform = nil, ruby_abi = nil)
if platform
public_versions.find_by(number:, platform:)
public_versions.find_by(number:, platform:, ruby_abi:)
else
public_versions.find_by(number:)
end
end

def public_version_payload(number, platform = nil)
version = find_public_version(number, platform)
def public_version_payload(number, platform = nil, ruby_abi = nil)
version = find_public_version(number, platform, ruby_abi)
payload(version).merge!(version.as_json) if version
end

Expand Down Expand Up @@ -263,6 +263,7 @@ def payload(version = most_recent_version, protocol = Gemcutter::PROTOCOL, host_
"version_created_at" => version.created_at,
"version_downloads" => version.downloads_count,
"platform" => version.platform,
"ruby_abi" => version.ruby_abi,
"authors" => version.authors,
"info" => version.info,
"licenses" => version.licenses,
Expand Down
9 changes: 7 additions & 2 deletions app/models/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Version < ApplicationRecord # rubocop:disable Metrics/ClassLength
validates :sha256, presence: true, if: :content_addressable?
validates :content_address, format: { with: CONTENT_ADDRESS_FORMAT }, allow_nil: true
validates :content_address, absence: true, unless: :content_addressable?
validates :ruby_abi, format: { with: /\A\d+\.\d+\z/ }, allow_nil: true

validates :number, :platform, :gem_platform, :full_name, :gem_full_name, :canonical_number,
name_format: { requires_letter: false },
Expand Down Expand Up @@ -147,6 +148,7 @@ def self.rows_for_index
joins(:rubygem)
.indexed
.release
.where(ruby_abi: nil)
.order("rubygems.name asc, position desc")
.pluck("rubygems.name", :number, :platform)
end
Expand All @@ -155,6 +157,7 @@ def self.rows_for_latest_index
joins(:rubygem)
.indexed
.latest
.where(ruby_abi: nil)
.order("rubygems.name asc, position desc")
.pluck("rubygems.name", :number, :platform)
end
Expand All @@ -163,6 +166,7 @@ def self.rows_for_prerelease_index
joins(:rubygem)
.indexed
.prerelease
.where(ruby_abi: nil)
.order("rubygems.name asc, position desc")
.pluck("rubygems.name", :number, :platform)
end
Expand Down Expand Up @@ -367,6 +371,7 @@ def payload
"rubygems_version" => required_rubygems_version,
"ruby_version" => required_ruby_version,
"prerelease" => prerelease,
"ruby_abi" => ruby_abi,
"licenses" => licenses,
"requirements" => requirements,
"sha" => sha256_hex,
Expand Down Expand Up @@ -656,8 +661,8 @@ def create_link_verifications
end

def record_push_event
rubygem.record_event!(Events::RubygemEvent::VERSION_PUSHED, number: number, platform: platform, sha256: sha256_hex,
pushed_by: pusher&.display_handle, version_gid: to_gid, actor_gid: pusher&.to_gid)
rubygem.record_event!(Events::RubygemEvent::VERSION_PUSHED, number: number, platform: platform, ruby_abi: ruby_abi,
sha256: sha256_hex, pushed_by: pusher&.display_handle, version_gid: to_gid, actor_gid: pusher&.to_gid)
end

def enqueue_web_hook_jobs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class Events::RubygemEvent::Version::PushedComponent < Events::TableDetailsCompo
def view_template
div do
t(".version_html", version:
link_to_version_from_gid(additional.version_gid, additional.number, additional.platform))
link_to_version_from_gid(additional.version_gid, additional.number, additional.platform, additional.ruby_abi))
end
if additional.sha256.present?
sha256 = capture { code(class: "tw-break-all") { additional.sha256 } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

class Events::RubygemEvent::Version::UnyankedComponent < Events::TableDetailsComponent
def view_template
raw t(".version_html", version: link_to_version_from_gid(additional.version_gid, additional.number, additional.platform))
raw t(".version_html", version: link_to_version_from_gid(additional.version_gid, additional.number, additional.platform, additional.ruby_abi))
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Events::RubygemEvent::Version::YankForbiddenComponent < Events::TableDetai
def view_template
div do
t(".version_html", version:
link_to_version_from_gid(additional.version_gid, additional.number, additional.platform))
link_to_version_from_gid(additional.version_gid, additional.number, additional.platform, additional.ruby_abi))
end
return if additional.yanked_by.blank?
div do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Events::RubygemEvent::Version::YankedComponent < Events::TableDetailsCompo
def view_template
div do
t(".version_html", version:
link_to_version_from_gid(additional.version_gid, additional.number, additional.platform))
link_to_version_from_gid(additional.version_gid, additional.number, additional.platform, additional.ruby_abi))
end
return if additional.yanked_by.blank?
div do
Expand Down
7 changes: 5 additions & 2 deletions app/views/components/events/table_details_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ def link_to_user_from_gid(gid, text)
end
end

def link_to_version_from_gid(gid, number, platform)
def link_to_version_from_gid(gid, number, platform, ruby_abi = nil)
version = load_gid(gid, only: Version)

if version
view_context.link_to version.to_title, rubygem_version_path(version.rubygem.slug, version.slug)
else
"#{rubygem.name} (#{number}#{"-#{platform}" unless platform.blank? || platform == 'ruby'})"
title = "#{rubygem.name} (#{number}#{"-#{platform}" unless platform.blank? || platform == 'ruby'}"
title << ", Ruby ABI #{ruby_abi}" if ruby_abi.present?
title << ")"
title
end
end

Expand Down
41 changes: 40 additions & 1 deletion test/functional/api/v2/versions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,45 @@ def self.should_respond_to(format)
create(:version, rubygem: @rubygem2, number: "1.0.0")
end

context "with versions across Ruby ABIs" do
setup do
@rubygem = create(:rubygem, name: "sandworm")
@abi32 = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl",
required_ruby_version: "~> 3.2.0", ruby_abi: "3.2", sha256: Digest::SHA2.base64digest("sandworm-1.0.0-3.2"))
@abi34 = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl",
required_ruby_version: "~> 3.4.0", ruby_abi: "3.4", sha256: Digest::SHA2.base64digest("sandworm-1.0.0-3.4"))
@multi = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl",
required_ruby_version: ">= 3.2", sha256: Digest::SHA2.base64digest("sandworm-1.0.0-multi"),
spec_sha256: Digest::SHA2.base64digest("spec-multi"))
end

should "return the targeted Ruby ABI variant when ruby_abi is given" do
get :show, params: { rubygem_name: @rubygem.name, number: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.2", format: "json" }

assert_response :success
payload = JSON.parse(@response.body)

assert_equal "3.2", payload["ruby_abi"]
assert_equal @abi32.sha256_hex, payload["sha"]
end

should "return the version supporting multiple Ruby ABIs without a ruby_abi param" do
get :show, params: { rubygem_name: @rubygem.name, number: "1.0.0", platform: "x86_64-linux-musl", format: "json" }

assert_response :success
payload = JSON.parse(@response.body)

assert_nil payload["ruby_abi"]
assert_equal @multi.sha256_hex, payload["sha"]
end

should "return not found for a ruby_abi that does not match any variant" do
get :show, params: { rubygem_name: @rubygem.name, number: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.3", format: "json" }

assert_response :not_found
end
end

should_respond_to(:json) do |body|
JSON.parse(body)
end
Expand Down Expand Up @@ -219,7 +258,7 @@ def self.should_respond_to(format)
assert_equal(
%w[
name downloads version version_created_at version_downloads platform
authors info licenses metadata yanked sha spec_sha project_uri gem_uri
ruby_abi authors info licenses metadata yanked sha spec_sha project_uri gem_uri
homepage_uri wiki_uri documentation_uri mailing_list_uri
source_code_uri bug_tracker_uri changelog_uri funding_uri dependencies
built_at created_at description downloads_count number summary
Expand Down
1 change: 1 addition & 0 deletions test/models/rubygem_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ class RubygemTest < ActiveSupport::TestCase
assert_equal @rubygem.most_recent_version.created_at.as_json, hash["version_created_at"]
assert_equal @rubygem.most_recent_version.downloads_count, hash["version_downloads"]
assert_equal @rubygem.most_recent_version.platform, hash["platform"]
assert_nil hash.fetch("ruby_abi")
assert_equal @rubygem.most_recent_version.authors, hash["authors"]
assert_equal @rubygem.most_recent_version.info, hash["info"]
assert_equal @rubygem.most_recent_version.metadata, hash["metadata"]
Expand Down
32 changes: 30 additions & 2 deletions test/models/version_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class VersionTest < ActiveSupport::TestCase
json = @version.as_json

fields = %w[number built_at summary description authors platform
ruby_version rubygems_version prerelease downloads_count licenses
ruby_version rubygems_version prerelease ruby_abi downloads_count licenses
requirements sha spec_sha metadata created_at]

assert_equal fields.sort, json.keys.sort
Expand Down Expand Up @@ -44,7 +44,7 @@ class VersionTest < ActiveSupport::TestCase
should "only have relevant API fields" do
xml = Nokogiri.parse(@version.to_xml)
fields = %w[number built-at summary description authors platform
ruby-version rubygems-version prerelease downloads-count licenses
ruby-version rubygems-version prerelease ruby-abi downloads-count licenses
requirements sha spec-sha metadata created-at]

assert_equal fields.map(&:to_s).sort,
Expand Down Expand Up @@ -159,6 +159,26 @@ class VersionTest < ActiveSupport::TestCase
refute_predicate @dup_version, :valid?
end

should "record the Ruby ABI on the pushed version event" do
version = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux", gem_platform: "x86_64-linux",
required_ruby_version: "~> 3.4.0", ruby_abi: "3.4",
sha256: Digest::SHA2.base64digest("abi-event-1.0.0"))

pushed = @rubygem.events.where(tag: Events::RubygemEvent::VERSION_PUSHED).sole

assert_equal "3.4", pushed.additional.ruby_abi
assert_equal version.sha256_hex, pushed.additional.sha256
end

should "not allow a ruby_abi that is not a Ruby minor version" do
version = build(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux", gem_platform: "x86_64-linux",
required_ruby_version: "~> 3.4.0", ruby_abi: "banana",
sha256: Digest::SHA2.base64digest("abi-format-1.0.0"))

refute_predicate version, :valid?
assert_includes version.errors[:ruby_abi], "is invalid"
end

should "allow duplicate versions with different Ruby ABIs" do
existing_version = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "arm64-darwin-25",
required_ruby_version: "~> 3.3.0", ruby_abi: "3.3")
Expand Down Expand Up @@ -1342,6 +1362,14 @@ class VersionTest < ActiveSupport::TestCase
number: "0.0.2.pre",
platform: "java",
prerelease: true)
@content_addressable_version = create(:version,
rubygem: @second_rubygem,
number: "0.0.2",
platform: "x86_64-linux-musl",
gem_platform: "x86_64-linux-musl",
required_ruby_version: "~> 3.4.0",
ruby_abi: "3.4",
sha256: Digest::SHA2.base64digest("second-0.0.2-x86_64-linux-musl"))
end

should "select only name, version, and platform for all gems" do
Expand Down
Loading