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
1 change: 1 addition & 0 deletions app/avo/resources/deletion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def fields
field :rubygem, as: :text
field :number, as: :text
field :platform, as: :text
field :ruby_abi, as: :text, title: "Ruby ABI"
field :user, as: :belongs_to
field :version, as: :belongs_to
end
Expand Down
12 changes: 10 additions & 2 deletions app/controllers/api/v1/deletions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,17 @@ def validate_gem_and_version
begin
version = params.expect(:version)
platform = params.permit(:platform).fetch(:platform, nil)
@version = @rubygem.find_version!(number: version, platform: platform)
ruby_abi = params.permit(:ruby_abi).fetch(:ruby_abi, nil).presence

if ruby_abi.present? && platform.blank?
return render plain: response_with_mfa_warning("The platform param is required when ruby_abi is specified."),
status: :bad_request
end

@version = @rubygem.find_version!(number: version, platform: platform, ruby_abi: ruby_abi)
rescue ActiveRecord::RecordNotFound
render plain: response_with_mfa_warning("The version #{version}#{" (#{platform})" if platform.present?} does not exist."),
details = "#{" (#{platform})" if platform.present?}#{" (Ruby ABI #{ruby_abi})" if ruby_abi.present?}"
render plain: response_with_mfa_warning("The version #{version}#{details} does not exist."),
status: :not_found
end
end
Expand Down
7 changes: 5 additions & 2 deletions app/models/deletion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def record_yank_forbidden_event!
reason: ineligible_reason,
number: version.number,
platform: version.platform,
ruby_abi: version.ruby_abi,
yanked_by: user.display_handle,
actor_gid: user.to_gid,
version_gid: version.to_gid
Expand All @@ -75,6 +76,7 @@ def metadata_matches_version
errors.add(:rubygem, "does not match version rubygem name") unless rubygem == version.rubygem.name
errors.add(:number, "does not match version number") unless number == version.number
errors.add(:platform, "does not match version platform") unless platform == version.platform
errors.add(:ruby_abi, "does not match version Ruby ABI") unless ruby_abi == version.ruby_abi
end

def rubygem_name
Expand All @@ -85,6 +87,7 @@ def record_metadata
self.rubygem = rubygem_name
self.number = version.number
self.platform = version.platform
self.ruby_abi = version.ruby_abi
end

def expire_cache
Expand Down Expand Up @@ -155,11 +158,11 @@ def send_gem_yanked_mail

def record_yank_event
version.rubygem.record_event!(Events::RubygemEvent::VERSION_YANKED, number: version.number, platform: version.platform,
yanked_by: user&.display_handle, actor_gid: user&.to_gid, version_gid: version.to_gid, force:)
ruby_abi: version.ruby_abi, yanked_by: user&.display_handle, actor_gid: user&.to_gid, version_gid: version.to_gid, force:)
end

def record_unyank_event
version.rubygem.record_event!(Events::RubygemEvent::VERSION_UNYANKED, number: version.number, platform: version.platform,
version_gid: version.to_gid)
ruby_abi: version.ruby_abi, version_gid: version.to_gid)
end
end
3 changes: 3 additions & 0 deletions app/models/events/rubygem_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Events::RubygemEvent < ApplicationRecord
VERSION_YANKED = define_event "rubygem:version:yanked" do
attribute :number, :string
attribute :platform, :string
attribute :ruby_abi, :string

attribute :yanked_by, :string

Expand All @@ -30,6 +31,7 @@ class Events::RubygemEvent < ApplicationRecord
VERSION_YANK_FORBIDDEN = define_event "rubygem:version:yank_forbidden" do
attribute :number, :string
attribute :platform, :string
attribute :ruby_abi, :string

attribute :yanked_by, :string

Expand All @@ -41,6 +43,7 @@ class Events::RubygemEvent < ApplicationRecord
VERSION_UNYANKED = define_event "rubygem:version:unyanked" do
attribute :number, :string
attribute :platform, :string
attribute :ruby_abi, :string

attribute :version_gid, :global_id
end
Expand Down
4 changes: 2 additions & 2 deletions app/models/rubygem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ def public_version_payload(number, platform = nil)
payload(version).merge!(version.as_json) if version
end

def find_version!(number:, platform:)
def find_version!(number:, platform:, ruby_abi: nil)
platform = platform.presence || "ruby"
versions.find_by!(number: number, platform: platform)
versions.find_by!(number: number, platform: platform, ruby_abi: ruby_abi)
end

def find_version_by_slug!(slug)
Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20260811160835_add_ruby_abi_to_deletions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddRubyAbiToDeletions < ActiveRecord::Migration[8.1]
def change
add_column :deletions, :ruby_abi, :string
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 2026_07_23_061553) do
ActiveRecord::Schema[8.1].define(version: 2026_08_11_160835) do
# These are extensions that must be enabled in order to support this database
enable_extension "hstore"
enable_extension "pg_catalog.plpgsql"
Expand Down Expand Up @@ -148,6 +148,7 @@
t.datetime "created_at", precision: nil, null: false
t.string "number"
t.string "platform"
t.string "ruby_abi"
t.string "rubygem"
t.datetime "updated_at", precision: nil, null: false
t.integer "user_id"
Expand Down
16 changes: 10 additions & 6 deletions script/restore_version
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

gem_name, version_number, platform = *ARGV
gem_name, version_number, platform, ruby_abi = *ARGV

abort "Usage: script/restore_version GEM_NAME VESRION_NUMBER [PLATFORM]" if gem_name.nil? || version_number.nil?
abort "Usage: script/restore_version GEM_NAME VERSION_NUMBER [PLATFORM] [RUBY_ABI]" if gem_name.nil? || version_number.nil?

ENV["RAILS_ENV"] ||= "production"
require_relative "../config/environment"

rubygem = Rubygem.find_by_name!(gem_name)
slug = version_number
slug = version_number.dup
slug << "-#{platform}" if platform.present?
version = rubygem.find_version!(number: version_number, platform: platform)
raise "Version #{slug} for #{gem_name} was not found" unless version
slug << " (Ruby ABI #{ruby_abi})" if ruby_abi.present?
begin
version = rubygem.find_version!(number: version_number, platform: platform, ruby_abi: ruby_abi.presence)
rescue ActiveRecord::RecordNotFound
abort "Version #{slug} for #{gem_name} was not found"
end

deletion = Deletion.find_by(rubygem: gem_name, number: version_number, platform: version.platform)
deletion = Deletion.find_by(rubygem: gem_name, number: version_number, platform: version.platform, ruby_abi: version.ruby_abi)
raise "Deletion record for version: #{version.full_name} was not found" unless deletion

deletion.version = version
Expand Down
125 changes: 125 additions & 0 deletions test/functional/api/v1/deletions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,131 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase
end
end

context "for a gem with content-addressable 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"))
create(:ownership, user: @user, rubygem: @rubygem)
RubygemFs.instance.store("gems/#{@abi32.full_name}.gem", "")
RubygemFs.instance.store("gems/#{@abi34.full_name}.gem", "")
end

context "ON DELETE with a ruby_abi param" do
setup do
delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.2" }
end

should respond_with :success

should "yank only the targeted Ruby ABI variant" do
refute_predicate @abi32.reload, :indexed?
assert_predicate @abi34.reload, :indexed?
end

should "respond with the deleted variant including its content address and Ruby ABI" do
assert_includes @response.body, "Successfully deleted gem: sandworm (1.0.0-724c5206, Platform: x86_64-linux-musl, Ruby ABI 3.2)"
end
end

context "ON DELETE without a ruby_abi param" do
setup do
delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl" }
end

should respond_with :not_found

should "not yank either Ruby ABI variant" do
assert_predicate @abi32.reload, :indexed?
assert_predicate @abi34.reload, :indexed?
end

should "respond that the version does not exist" do
assert_includes @response.body, "The version 1.0.0 (x86_64-linux-musl) does not exist."
end
end

context "ON DELETE with a ruby_abi param that does not match any variant" do
setup do
delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.3" }
end

should respond_with :not_found

should "not yank either Ruby ABI variant" do
assert_predicate @abi32.reload, :indexed?
assert_predicate @abi34.reload, :indexed?
end

should "respond that the version does not exist" do
assert_includes @response.body, "The version 1.0.0 (x86_64-linux-musl) (Ruby ABI 3.3) does not exist."
end
end

context "ON DELETE with a ruby_abi param but no platform" do
setup do
delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", ruby_abi: "3.2" }
end

should respond_with :bad_request

should "not yank either Ruby ABI variant" do
assert_predicate @abi32.reload, :indexed?
assert_predicate @abi34.reload, :indexed?
end

should "respond that the platform param is required" do
assert_includes @response.body, "The platform param is required when ruby_abi is specified."
end
end

context "when a version supporting multiple Ruby ABIs coexists" do
setup do
@multi_abi = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl",
required_ruby_version: ">= 3.2")
RubygemFs.instance.store("gems/#{@multi_abi.full_name}.gem", "")
end

context "ON DELETE with only a platform param" do
setup do
delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl" }
end

should respond_with :success

should "yank only the version supporting multiple Ruby ABIs and keep the single ABI variants" do
refute_predicate @multi_abi.reload, :indexed?
assert_predicate @abi32.reload, :indexed?
assert_predicate @abi34.reload, :indexed?
end

should "respond with the deleted platform version" do
assert_includes @response.body, "Successfully deleted gem: sandworm (1.0.0-x86_64-linux-musl)"
end
end

context "ON DELETE with an empty ruby_abi param" do
setup do
delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "" }
end

should respond_with :success

should "treat the empty ruby_abi as absent and yank the version supporting multiple Ruby ABIs" do
refute_predicate @multi_abi.reload, :indexed?
assert_predicate @abi32.reload, :indexed?
assert_predicate @abi34.reload, :indexed?
end

should "respond with the deleted platform version" do
assert_includes @response.body, "Successfully deleted gem: sandworm (1.0.0-x86_64-linux-musl)"
end
end
end
end

context "for a gem SomeGem with a version 0.1.0" do
setup do
@rubygem = create(:rubygem, name: "SomeGem")
Expand Down
10 changes: 10 additions & 0 deletions test/models/deletion_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,16 @@ class DeletionTest < ActiveSupport::TestCase

assert_equal deletion.rubygem, @version.rubygem.name
assert_equal @version.id, deletion.version_id
assert_nil deletion.ruby_abi
end

should "record the Ruby ABI for versions targeting a single Ruby ABI" do
version = create(:version, rubygem: @version.rubygem, number: "2.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("test-2.0.0-3.4"))
deletion = Deletion.new(version: version, user: @user)
deletion.valid?

assert_equal "3.4", deletion.ruby_abi
end

context "with restored gem" do
Expand Down
13 changes: 13 additions & 0 deletions test/models/rubygem_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ class RubygemTest < ActiveSupport::TestCase
assert_equal version3_ruby, @rubygem.most_recent_version
end

should "find versions by number, platform and Ruby ABI" do
plain = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl",
required_ruby_version: ">= 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("abi34-1.0.0"))

assert_equal plain, @rubygem.find_version!(number: "1.0.0", platform: "x86_64-linux-musl")
assert_equal abi34, @rubygem.find_version!(number: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.4")
assert_raises(ActiveRecord::RecordNotFound) do
@rubygem.find_version!(number: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.2")
end
end

should "mark the latest version for each Ruby ABI per platform" do
abi32_old = 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("abi32-1.0.0"))
Expand Down
2 changes: 1 addition & 1 deletion test/system/avo/versions_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class Avo::VersionsSystemTest < ApplicationSystemTestCase
"updated_at" => [deletion.updated_at.as_json, nil],
"version_id" => [version.id, nil]
},
"unchanged" => {}
"unchanged" => { "ruby_abi" => nil }
},
version_unyank_event.to_gid.to_s => {
"changes" => version_unyank_event.attributes.transform_values { [nil, it] }.as_json,
Expand Down
Loading