From 2ddb807be31245b90cea952d337b4bf9ff57d0f9 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Tue, 28 Jul 2026 15:52:28 +0100 Subject: [PATCH 1/3] RepologyIndex: index sibling-formula projects instead of skipping them A Repology project that groups Homebrew formulae with distinct base names (wget + wget2, sqlite + sqlite-analyzer, boost + boost-mpi, ffmpeg + a third-party ffmpeg-full) was previously omitted from the index and recorded only in meta.ambiguous_projects. That left 50 projects unmapped, including several heavy-CVE targets (ffmpeg, imagemagick, wget, sqlite, binutils, openssl variants). Each formula in such a project is now indexed with the full distro srcname set. In Homebrew::Vulns::Match the sibling's srcnames become extra low-confidence distro queries whose upstream-CVE range check will not match this formula's identity, so the cost is uncomparable candidate records rather than wrong :affected/:fixed claims. meta.ambiguous_projects is still populated so the set can be tightened via repology-rules PRs. --- lib/repology_index.rb | 26 ++++++++++++++++---------- test/repology_index_test.rb | 20 ++++++++++++-------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/repology_index.rb b/lib/repology_index.rb index d203fa1b5f..e2d044e8fc 100644 --- a/lib/repology_index.rb +++ b/lib/repology_index.rb @@ -86,10 +86,7 @@ def build next if distros.empty? names = brew_entries.keys - if self.class.ambiguous_homebrew_set?(names) - ambiguous[project] = names.sort - next - end + ambiguous[project] = names.sort if self.class.ambiguous_homebrew_set?(names) brew_entries.each do |formula, preferred| contributions[formula][project] ||= Contribution.new(project:, preferred:, distros:) end @@ -97,7 +94,11 @@ def build formulae, colliding = resolve(contributions) - log_skipped("ambiguous project", ambiguous.keys) unless ambiguous.empty? + unless ambiguous.empty? + log "#{ambiguous.size} projects group sibling formulae " \ + "(indexed with shared srcnames; tighten via repology-rules PR): " \ + "#{ambiguous.keys.sort.join(", ")}" + end log_skipped("cross-project formula", colliding.keys) unless colliding.empty? { @@ -236,11 +237,16 @@ def self.homebrew_entries(entries) result end - # A Repology project can group unrelated Homebrew formulae (e.g. `antlr` and - # `antlr4-cpp-runtime`) whose distro packages must not be cross-attributed. - # Versioned variants of one formula (`ant`, `ant@1.9`) are the same upstream - # and are safe to fan out. Ambiguous projects are recorded in - # `meta.ambiguous_projects`; the fix is a repology-rules PR upstream. + # A Repology project can group Homebrew formulae with distinct base names + # (`wget` + `wget2`, `boost` + `boost-mpi`, `ffmpeg` + a third-party + # `ffmpeg-full`). Such projects are still indexed: each formula gets the full + # distro srcname set, and the sibling's srcnames become extra low-confidence + # distro queries in `Homebrew::Vulns::Match` whose upstream-CVE range check + # will not match this formula's identity, so the cost is uncomparable + # candidates rather than wrong `:affected`/`:fixed` claims. The set is + # recorded in `meta.ambiguous_projects` so it can be tightened via a + # repology-rules PR. Versioned variants of one formula (`ant`, `ant@1.9`) + # are not counted as ambiguous. def self.ambiguous_homebrew_set?(names) names.map { |n| n.sub(/@.+\z/, "") }.uniq.size > 1 end diff --git a/test/repology_index_test.rb b/test/repology_index_test.rb index ab3cb31931..5478babfe5 100644 --- a/test/repology_index_test.rb +++ b/test/repology_index_test.rb @@ -193,7 +193,8 @@ def test_build_paginates_by_max_key_and_aggregates_all_homebrew_names "/projects/#{URI.encode_uri_component(max_key)}/?inrepo=homebrew"], calls, ) - assert_equal ["ack", "ant", "ant@1.9", "curl", "libgee", "zstd"], result["formulae"].keys + assert_equal ["ack", "ant", "ant@1.9", "antlr", "antlr4-cpp-runtime", "curl", "libgee", "zstd"], + result["formulae"].keys assert_equal result["formulae"]["ant"], result["formulae"]["ant@1.9"] assert_equal( { "AlmaLinux" => ["zstd"], "Mageia" => ["zstd"], "Rocky Linux" => ["zstd"] }, @@ -205,7 +206,8 @@ def test_build_stops_on_first_short_page fetcher, calls = recording_fetcher("/projects/?inrepo=homebrew" => fixture("repology_page1.json")) result = new_index(fetcher:).build assert_equal 1, calls.size - assert_equal ["ack", "ant", "ant@1.9", "curl", "libgee"], result["formulae"].keys + assert_equal ["ack", "ant", "ant@1.9", "antlr", "antlr4-cpp-runtime", "curl", "libgee"], + result["formulae"].keys end def test_build_respects_page_limit @@ -221,15 +223,16 @@ def test_build_omits_formulae_with_no_mapped_distros refute_includes new_index(fetcher:).build["formulae"], "brew-only-tool" end - def test_build_records_ambiguous_projects_and_omits_their_formulae + def test_build_records_ambiguous_projects_and_indexes_each_sibling_with_shared_srcnames fetcher, = recording_fetcher("/projects/?inrepo=homebrew" => fixture("repology_page1.json")) result = new_index(fetcher:).build - refute_includes result["formulae"], "antlr" - refute_includes result["formulae"], "antlr4-cpp-runtime" + expected = { "Debian" => ["antlr3", "antlr4", "antlr4-cpp-runtime"] } + assert_equal expected, result["formulae"]["antlr"] + assert_equal expected, result["formulae"]["antlr4-cpp-runtime"] assert_equal({ "antlr" => ["antlr", "antlr4-cpp-runtime"] }, result["meta"]["ambiguous_projects"]) end - def test_build_logs_ambiguous_and_colliding_skips + def test_build_logs_ambiguous_and_colliding logger = StringIO.new page = fixture("repology_page1.json").merge( "p1" => [{ "repo" => "homebrew", "srcname" => "foo", "status" => "newest" }, @@ -239,7 +242,7 @@ def test_build_logs_ambiguous_and_colliding_skips ) fetcher, = recording_fetcher("/projects/?inrepo=homebrew" => page) RepologyIndex.new(fetcher:, sleeper: ->(_) {}, logger:).build - assert_match(/skipped 1 ambiguous project .*repology-rules.*: antlr\b/, logger.string) + assert_match(/1 projects group sibling formulae .*repology-rules.*: antlr\b/, logger.string) assert_match(/skipped 1 cross-project formula .*: foo\b/, logger.string) end @@ -257,7 +260,8 @@ def test_write_creates_output_directory_and_emits_pretty_json fetcher, = recording_fetcher("/projects/?inrepo=homebrew" => fixture("repology_page1.json")) new_index(fetcher:).write(out) parsed = JSON.parse(File.read(out)) - assert_equal ["ack", "ant", "ant@1.9", "curl", "libgee"], parsed["formulae"].keys + assert_equal ["ack", "ant", "ant@1.9", "antlr", "antlr4-cpp-runtime", "curl", "libgee"], + parsed["formulae"].keys assert File.read(out).end_with?("\n") end end From 07a888bde0f15e38db92fa69205037cb2077ebd1 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Tue, 28 Jul 2026 15:52:29 +0100 Subject: [PATCH 2/3] validate.yml: run rake test on push and PR regenerate.yml runs the test suite before rebuilding the index, but only on schedule; PRs touching lib/ or test/ had no test coverage. Add a test job mirroring regenerate.yml's Ruby setup. --- .github/workflows/validate.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9387048a16..c8ce99681d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -27,3 +27,24 @@ jobs: - name: Validate advisories against OSV schema run: check-jsonschema --schemafile osv-schema.json advisories/*.json + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + + - name: Set up Ruby + uses: Homebrew/actions/setup-ruby@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + + - name: Run tests + run: rake test + + # RuboCop is not run: the org-synced .rubocop.yml sets + # `AllCops: Include: ["**/*.rbi"]` which replaces (not extends) the + # default .rb globs, so it scans nothing in this repository. Fix belongs + # in the Homebrew/.github sync source. From f3075407e150a6432cb0a05f9b15a59aac760ca4 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Tue, 28 Jul 2026 16:10:24 +0100 Subject: [PATCH 3/3] RepologyIndex: singular/plural in sibling-project log line --- lib/repology_index.rb | 2 +- test/repology_index_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/repology_index.rb b/lib/repology_index.rb index e2d044e8fc..cd2a46937f 100644 --- a/lib/repology_index.rb +++ b/lib/repology_index.rb @@ -95,7 +95,7 @@ def build formulae, colliding = resolve(contributions) unless ambiguous.empty? - log "#{ambiguous.size} projects group sibling formulae " \ + log "#{ambiguous.size} project#{"s" unless ambiguous.one?} grouping sibling formulae " \ "(indexed with shared srcnames; tighten via repology-rules PR): " \ "#{ambiguous.keys.sort.join(", ")}" end diff --git a/test/repology_index_test.rb b/test/repology_index_test.rb index 5478babfe5..85ad01aeb8 100644 --- a/test/repology_index_test.rb +++ b/test/repology_index_test.rb @@ -242,7 +242,7 @@ def test_build_logs_ambiguous_and_colliding ) fetcher, = recording_fetcher("/projects/?inrepo=homebrew" => page) RepologyIndex.new(fetcher:, sleeper: ->(_) {}, logger:).build - assert_match(/1 projects group sibling formulae .*repology-rules.*: antlr\b/, logger.string) + assert_match(/1 project grouping sibling formulae .*repology-rules.*: antlr\b/, logger.string) assert_match(/skipped 1 cross-project formula .*: foo\b/, logger.string) end