From d8a827b91e0a2e34776a7e71f0603df5333d3bc9 Mon Sep 17 00:00:00 2001 From: Szymon Kurcab Date: Fri, 18 Sep 2026 20:41:37 +0200 Subject: [PATCH] fix: do not read a rubygems.org outage as "never published" `published` asked rubygems.org whether this version is already out and rescued OpenURI::HTTPError to false. The comment says 404, and 404 does mean the gem has never been published -- but the rescue caught every HTTP error. A 500, a 429, or anything else from rubygems.org read as "not published", which is the one answer that makes release:push go ahead and push. That inverts the check. It exists so a re-run is a no-op, and the case it is least able to handle is the case where it cannot tell. Failing the release is the right answer there: the version is still sitting in the gemspec and the workflow can be run again. Only 404 returns false now; everything else propagates. The lookup gets explicit open and read timeouts instead of inheriting none, and the response is checked for being the array it is parsed as -- .any? over an unexpected Hash would have raised NoMethodError from inside a lambda with no context. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 9 +++++++++ Rakefile | 20 ++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 846df76..1c679e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +- `rake release:status` and `rake release:push` no longer read every HTTP + error from rubygems.org as "this version was never published". Only a 404 + means that; a 500, a rate limit, or a timeout now raises and fails the + release rather than falling through to a `gem push` of a version that may + already be out. The lookup also has explicit timeouts and checks that the + response is the array it expects. + ## 0.1.0 - 2026-09-18 Provider-neutral release. One `Client`, two providers behind it. diff --git a/Rakefile b/Rakefile index 786f95c..a4b3064 100644 --- a/Rakefile +++ b/Rakefile @@ -16,13 +16,25 @@ task default: :test namespace :release do gemspec = -> { Gem::Specification.load(Dir["*.gemspec"].first) } + # Whether this version is already on RubyGems. Only a 404 counts as "no": + # it is the answer to "has this gem ever been published", and it is the one + # HTTP error that means anything here. A 500, a rate limit, or a timeout is + # not an answer, and treating it as one would have release:push fall through + # to a `gem push` of something that may already be out there -- which is + # exactly the case this check exists to avoid. published = lambda do |spec| require "open-uri" require "json" - body = URI.open("https://rubygems.org/api/v1/versions/#{spec.name}.json", &:read) - JSON.parse(body).any? { |v| v["number"] == spec.version.to_s } - rescue OpenURI::HTTPError - false # 404 => the gem has never been published, so there is nothing to skip + url = "https://rubygems.org/api/v1/versions/#{spec.name}.json" + body = URI.parse(url).open(open_timeout: 10, read_timeout: 30, &:read) + versions = JSON.parse(body) + raise "rubygems.org returned #{versions.class} for #{url}, expected an array" unless versions.is_a?(Array) + + versions.any? { |v| v.is_a?(Hash) && v["number"] == spec.version.to_s } + rescue OpenURI::HTTPError => e + raise unless e.io.status.first.to_s == "404" + + false # never published, so there is nothing to skip end desc "Build the gem into pkg/ with --strict (proof the gemspec is valid). " \