From 85684f43bd3cf2d0a7350224bab262b47f750f1f Mon Sep 17 00:00:00 2001 From: Sam Ford <1584702+samford@users.noreply.github.com> Date: Sat, 3 May 2025 12:13:19 -0400 Subject: [PATCH 1/5] Update eol_data for API changes The endoflife.date API has been updated, so this modifies the URL in `SharedAudits.eol_data` to use the up to date URL and modifies the related logic in `FormulaAuditor.audit_eol` to work with the new response format. Specifically, there is now an `isEol` boolean value and the EOL date is found in `eolFrom`. One wrinkle of the new setup is that 404 responses now return HTML content even if the request includes an `Accept: application/json` header. This handles these types of responses by catching `JSON::ParserError` but ideally we would parse the response headers and use `Utils::Curl.http_status_ok?` to check for a good response status before trying to parse the response body as JSON. --- Library/Homebrew/formula_auditor.rb | 7 +++---- Library/Homebrew/utils/shared_audits.rb | 13 +++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Library/Homebrew/formula_auditor.rb b/Library/Homebrew/formula_auditor.rb index d555e99d22..1b17af76c6 100644 --- a/Library/Homebrew/formula_auditor.rb +++ b/Library/Homebrew/formula_auditor.rb @@ -612,11 +612,10 @@ module Homebrew metadata = SharedAudits.eol_data(name, formula.version.major.to_s) metadata ||= SharedAudits.eol_data(name, formula.version.major_minor.to_s) - return if metadata.blank? || (eol = metadata["eol"]).blank? + return if metadata.blank? || (metadata.dig("result", "isEol") != true) - is_eol = eol == true - is_eol ||= eol.is_a?(String) && (eol_date = Date.parse(eol)) <= Date.today - return unless is_eol + eol_from = metadata.dig("result", "eolFrom") + eol_date = Date.parse(eol_from) if eol_from.present? message = "Product is EOL" message += " since #{eol_date}" if eol_date.present? diff --git a/Library/Homebrew/utils/shared_audits.rb b/Library/Homebrew/utils/shared_audits.rb index 9ba8c9f9ac..2269e0ab1e 100644 --- a/Library/Homebrew/utils/shared_audits.rb +++ b/Library/Homebrew/utils/shared_audits.rb @@ -12,10 +12,15 @@ module SharedAudits def self.eol_data(product, cycle) @eol_data ||= T.let({}, T.nilable(T::Hash[String, T.untyped])) @eol_data["#{product}/#{cycle}"] ||= begin - result = Utils::Curl.curl_output("--location", "https://endoflife.date/api/#{product}/#{cycle}.json") - json = JSON.parse(result.stdout) if result.status.success? - json = nil if json&.dig("message")&.include?("Product not found") - json + result = Utils::Curl.curl_output("--location", "https://endoflife.date/api/v1/products/#{product}/releases/#{cycle}") + + if result.status.success? + begin + JSON.parse(result.stdout) + rescue JSON::ParserError + nil + end + end end end From 2493be79cc96e2266e4208cedb5a8f80021e5a75 Mon Sep 17 00:00:00 2001 From: Daeho Ro Date: Sun, 4 May 2025 02:10:08 +0900 Subject: [PATCH 2/5] utils/shared_audits: add eol_data test --- .../Homebrew/test/utils/shared_audits_spec.rb | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Library/Homebrew/test/utils/shared_audits_spec.rb b/Library/Homebrew/test/utils/shared_audits_spec.rb index 995fe9c79a..0f3badaa35 100644 --- a/Library/Homebrew/test/utils/shared_audits_spec.rb +++ b/Library/Homebrew/test/utils/shared_audits_spec.rb @@ -1,8 +1,62 @@ # frozen_string_literal: true require "utils/shared_audits" +require "utils/curl" RSpec.describe SharedAudits do + let(:eol_json_text) do + <<~JSON + { + "schema_version" : "1.0.0", + "generated_at": "2025-05-03T15:47:58+00:00", + "result": { + "name": "22", + "codename": null, + "label": "22 (LTS)", + "releaseDate": "2024-04-24", + "isLts": true, + "ltsFrom": "2024-10-29", + "isEoas": false, + "eoasFrom": "2025-10-21", + "isEol": false, + "eolFrom": "2027-04-30", + "isEoes": null, + "eoesFrom": null, + "isMaintained": true, + "latest": { + "name": "22.15.0", + "date": "2025-04-23", + "link": "https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V22.md#22.15.0" + } + } + } + JSON + end + + def mock_curl_output(stdout: "", success: true) + status = instance_double(Process::Status, success?: success) + curl_output = instance_double(SystemCommand::Result, stdout:, status:) + allow(Utils::Curl).to receive(:curl_output).and_return curl_output + end + + describe "::eol_data" do + it "returns the `isEol` and `eolFrom` values if the product is found" do + mock_curl_output stdout: eol_json_text + expect(described_class.eol_data("product", "cycle").dig("result", "isEol")).to be(false) + expect(described_class.eol_data("product", "cycle").dig("result", "eolFrom")).to eq("2027-04-30") + end + + it "returns nil if the product is not found" do + mock_curl_output stdout: "" + expect(described_class.eol_data("none", "cycle")).to be_nil + end + + it "returns nil if api call fails" do + mock_curl_output success: false + expect(described_class.eol_data("", "")).to be_nil + end + end + describe "::github_tag_from_url" do it "finds tags in archive urls" do url = "https://github.com/a/b/archive/refs/tags/v1.2.3.tar.gz" From 98b919f6721ec9dceda0c175861509af00b4001b Mon Sep 17 00:00:00 2001 From: Sam Ford <1584702+samford@users.noreply.github.com> Date: Sat, 3 May 2025 20:48:20 -0400 Subject: [PATCH 3/5] shared_audits_spec: use generic JSON values This updates `eol_json_text` to use generic values (instead of values from nodejs) and to omit some unused fields. --- .../Homebrew/test/utils/shared_audits_spec.rb | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/Library/Homebrew/test/utils/shared_audits_spec.rb b/Library/Homebrew/test/utils/shared_audits_spec.rb index 0f3badaa35..a4cde2d26d 100644 --- a/Library/Homebrew/test/utils/shared_audits_spec.rb +++ b/Library/Homebrew/test/utils/shared_audits_spec.rb @@ -8,25 +8,21 @@ RSpec.describe SharedAudits do <<~JSON { "schema_version" : "1.0.0", - "generated_at": "2025-05-03T15:47:58+00:00", + "generated_at": "2025-01-02T01:23:45+00:00", "result": { - "name": "22", + "name": "1.2", "codename": null, - "label": "22 (LTS)", - "releaseDate": "2024-04-24", - "isLts": true, - "ltsFrom": "2024-10-29", - "isEoas": false, - "eoasFrom": "2025-10-21", - "isEol": false, - "eolFrom": "2027-04-30", - "isEoes": null, - "eoesFrom": null, - "isMaintained": true, + "label": "1.2", + "releaseDate": "2024-01-01", + "isLts": false, + "ltsFrom": null, + "isEol": true, + "eolFrom": "2025-01-01", + "isMaintained": false, "latest": { - "name": "22.15.0", - "date": "2025-04-23", - "link": "https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V22.md#22.15.0" + "name": "1.0.0", + "date": "2024-01-01", + "link": "https://example.com/1.0.0" } } } @@ -40,10 +36,10 @@ RSpec.describe SharedAudits do end describe "::eol_data" do - it "returns the `isEol` and `eolFrom` values if the product is found" do + it "returns a parsed JSON object if the product is found" do mock_curl_output stdout: eol_json_text - expect(described_class.eol_data("product", "cycle").dig("result", "isEol")).to be(false) - expect(described_class.eol_data("product", "cycle").dig("result", "eolFrom")).to eq("2027-04-30") + expect(described_class.eol_data("product", "cycle").dig("result", "isEol")).to be(true) + expect(described_class.eol_data("product", "cycle").dig("result", "eolFrom")).to eq("2025-01-01") end it "returns nil if the product is not found" do From 53c0780d85f09c4c70948fb68bed426abad9325b Mon Sep 17 00:00:00 2001 From: Sam Ford <1584702+samford@users.noreply.github.com> Date: Sat, 3 May 2025 20:49:53 -0400 Subject: [PATCH 4/5] shared_audits_spec: guard against nil value The return value from `eol_data` can be `nil`, so we should use a safe navigation operator before `#dig`. --- Library/Homebrew/test/utils/shared_audits_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Library/Homebrew/test/utils/shared_audits_spec.rb b/Library/Homebrew/test/utils/shared_audits_spec.rb index a4cde2d26d..9d95b84ea9 100644 --- a/Library/Homebrew/test/utils/shared_audits_spec.rb +++ b/Library/Homebrew/test/utils/shared_audits_spec.rb @@ -38,8 +38,8 @@ RSpec.describe SharedAudits do describe "::eol_data" do it "returns a parsed JSON object if the product is found" do mock_curl_output stdout: eol_json_text - expect(described_class.eol_data("product", "cycle").dig("result", "isEol")).to be(true) - expect(described_class.eol_data("product", "cycle").dig("result", "eolFrom")).to eq("2025-01-01") + expect(described_class.eol_data("product", "cycle")&.dig("result", "isEol")).to be(true) + expect(described_class.eol_data("product", "cycle")&.dig("result", "eolFrom")).to eq("2025-01-01") end it "returns nil if the product is not found" do From 69dcbacb71d65b70e26f81b7c2a41cfc1cfff6a2 Mon Sep 17 00:00:00 2001 From: Sam Ford <1584702+samford@users.noreply.github.com> Date: Sat, 3 May 2025 21:15:11 -0400 Subject: [PATCH 5/5] shared_audits: prevent duplicate eol_data fetches The `eol_data` method uses `@eol_data["#{product}/#{cycle}"] ||=`, which can unncessarily allow a duplicate API call if the same product/cycle combination was previously tried but returned a 404 (Not Found) response. In this scenario, the value would be `nil` but the existing logic doesn't check whether this is a missing key or a `nil` value. If the key is present, we shouldn't make the same request again. This updates the method to return the existing value if the key exists, which effectively prevents duplicate fetches. This new logic only modifies `@eol_data` if `curl` is successful, so it does allow the request to be made again if it failed before. That said, this shouldn't normally be an issue and this is mostly about refactoring the method to allow for nicer code organization. This approach reduces the `begin` block to only the `JSON.parse` call, which allows us to use `return unless result.status.success?` (this previously led to a RuboCop offense because it was called within a `begin` block). --- Library/Homebrew/utils/shared_audits.rb | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Library/Homebrew/utils/shared_audits.rb b/Library/Homebrew/utils/shared_audits.rb index 2269e0ab1e..4dec1963cc 100644 --- a/Library/Homebrew/utils/shared_audits.rb +++ b/Library/Homebrew/utils/shared_audits.rb @@ -11,16 +11,19 @@ module SharedAudits sig { params(product: String, cycle: String).returns(T.nilable(T::Hash[String, T.untyped])) } def self.eol_data(product, cycle) @eol_data ||= T.let({}, T.nilable(T::Hash[String, T.untyped])) - @eol_data["#{product}/#{cycle}"] ||= begin - result = Utils::Curl.curl_output("--location", "https://endoflife.date/api/v1/products/#{product}/releases/#{cycle}") + key = "#{product}/#{cycle}" + return @eol_data[key] if @eol_data.key?(key) - if result.status.success? - begin - JSON.parse(result.stdout) - rescue JSON::ParserError - nil - end - end + result = Utils::Curl.curl_output( + "--location", + "https://endoflife.date/api/v1/products/#{product}/releases/#{cycle}", + ) + return unless result.status.success? + + @eol_data[key] = begin + JSON.parse(result.stdout) + rescue JSON::ParserError + nil end end