end_of_life 0.5.1 → 1.0.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ require "perfect_toml"
2
+
3
+ module EndOfLife
4
+ module Parsers
5
+ module MiseToml
6
+ extend self
7
+
8
+ def parse(file_content)
9
+ PerfectTOML.parse(file_content)["tools"]
10
+ rescue PerfectTOML::ParseError
11
+ nil
12
+ end
13
+ end
14
+ end
15
+ end
@@ -10,6 +10,7 @@ module EndOfLife
10
10
  line = line.strip
11
11
  next if line.start_with?("#") || line.empty?
12
12
 
13
+ line = line.split("#").first.strip # inline comments
13
14
  tool, version, * = line.split
14
15
 
15
16
  next if version == "latest"
@@ -1,16 +1,17 @@
1
1
  module EndOfLife
2
2
  module Product::Registry
3
- def scans_for(product_name, search_query:)
3
+ def scans_for(product_name, label: nil)
4
4
  product_registry[product_name.to_sym.downcase] = Product.new(
5
5
  product_name,
6
- search_query,
7
- version_detector_for(product_name)
6
+ version_detector_for(product_name),
7
+ label
8
8
  )
9
9
  end
10
10
 
11
11
  def find_product(name) = product_registry.fetch(name.to_sym.downcase)
12
12
 
13
13
  def products = product_registry.values
14
+ def products_pattern(suffix: nil) = /\A(?:#{products.map(&:name).join("|")})#{suffix}/i
14
15
 
15
16
  private
16
17
 
@@ -3,6 +3,17 @@ module EndOfLife
3
3
  class Release < Data.define(:product, :version, :eol_date)
4
4
  include Comparable
5
5
 
6
+ def self.parse!(string)
7
+ product, version = string.split("@", 2)
8
+ raise ArgumentError, "Invalid product release format: #{string}" if product.to_s.empty? || version.to_s.empty?
9
+
10
+ begin
11
+ new(product:, version:)
12
+ rescue ArgumentError
13
+ raise ArgumentError, "Malformed version number string: #{version}"
14
+ end
15
+ end
16
+
6
17
  def self.ruby(version, eol_date: nil) = new(product: "ruby", version:, eol_date:)
7
18
 
8
19
  def initialize(product:, version:, eol_date: nil)
@@ -19,12 +30,22 @@ module EndOfLife
19
30
  end
20
31
  end
21
32
 
33
+ def supported?(...) = !eol?(...)
34
+
35
+ def latest_release_in_cycle
36
+ product.all_releases.filter { |r| r.version.to_s.start_with?(cycle_version.to_s) }.max
37
+ end
38
+
39
+ def cycle_version
40
+ Gem::Version.new(version.segments.first(2).join("."))
41
+ end
42
+
22
43
  ZERO = Gem::Version.new("0")
23
44
  def zero? = version == ZERO
24
45
 
25
46
  def <=>(other) = version <=> other.version
26
47
 
27
- def to_s = version.to_s
48
+ def to_s = "#{product.name}@#{version}"
28
49
  end
29
50
  end
30
51
  end
@@ -2,31 +2,32 @@ module EndOfLife
2
2
  class Product
3
3
  def self.find(name) = EndOfLife.find_product(name)
4
4
 
5
- attr_reader :name, :search_query, :version_detector
5
+ attr_reader :name, :version_detector, :label
6
6
 
7
- def initialize(name, search_query, version_detector)
7
+ def initialize(name, version_detector, label = nil)
8
8
  @name = name.to_s.downcase
9
- @search_query = search_query
10
9
  @version_detector = version_detector
10
+ @label = label || @name.capitalize
11
11
  end
12
12
 
13
13
  def eol_releases_at(date)
14
- all_releases.filter { |release| release.eol_date <= date }
14
+ all_releases.filter { |release| release.eol_date.nil? || release.eol_date <= date }
15
15
  end
16
16
 
17
17
  def latest_eol_release(at: Date.today)
18
18
  eol_releases_at(at).max
19
19
  end
20
20
 
21
- def label = name.capitalize
22
- def to_s = label
23
-
24
- private
25
-
26
21
  def all_releases
27
22
  @all_releases ||= API.fetch_product(name)
28
23
  .dig(:result, :releases)
29
24
  .map { |json| Release.new(name, json[:latest][:name], json[:eolFrom]) }
30
25
  end
26
+
27
+ def search_query
28
+ version_detector.relevant_files.map { |f| %(filename:"#{f}") }.join(" ")
29
+ end
30
+
31
+ def to_s = label
31
32
  end
32
33
  end
@@ -14,11 +14,22 @@ module EndOfLife
14
14
  github.auto_paginate = true
15
15
  options[:user] ||= github.user.login
16
16
 
17
+ # GitHub doesn't have a way to get repos that contain specific files.
18
+ # The language filter sort of works, but it might miss some repos that
19
+ # use a language, but it's not the main one.
20
+ #
21
+ # We have to use the code search endpoint to find files matching the
22
+ # product we're interested in and then extract the repositories from
23
+ # the results.
17
24
  query = Query.new(options).to_s
18
- items = github.search_repositories(query, {sort: :updated}).items
25
+ repo_names = github.search_code(query).items.map { |item| item.repository.full_name }.uniq
26
+ return Success([]) if repo_names.empty?
27
+
28
+ repos_query = repo_names.map { |name| "repo:#{name}" }.join(" ")
29
+ repos = github.search_repositories(repos_query, {sort: :updated}).items
19
30
 
20
31
  Success(
21
- items.filter_map do |repo|
32
+ repos.filter_map do |repo|
22
33
  next if repo.archived && options[:skip_archived]
23
34
 
24
35
  Repository.new(
@@ -1,3 +1,6 @@
1
+ require "base64"
2
+ require "octokit"
3
+
1
4
  module EndOfLife
2
5
  class Repository
3
6
  class << self
@@ -0,0 +1,37 @@
1
+ require "stringio"
2
+
3
+ module EndOfLife
4
+ module Scanner
5
+ class Report < Data.define(:product, :repositories, :max_eol_date)
6
+ include Helpers::Text
7
+ include Helpers::Terminal
8
+
9
+ def to_s
10
+ report = StringIO.new
11
+ report.puts
12
+
13
+ if repositories.empty?
14
+ report.puts "No repositories using EOL #{product}."
15
+ else
16
+ report.puts "Found #{pluralize(repositories.size, "repository", "repositories")} using EOL #{product} (<= #{product.latest_eol_release(at: max_eol_date)}):"
17
+ report.puts end_of_life_table(repositories)
18
+ end
19
+
20
+ report.string
21
+ end
22
+
23
+ def failure? = repositories.any?
24
+
25
+ private
26
+
27
+ def end_of_life_table(repositories)
28
+ headers = ["", "Repository", "#{product} version"]
29
+ rows = repositories.map.with_index(1) do |repo, i|
30
+ [i, repo.url, repo.min_release_of(product)]
31
+ end
32
+
33
+ table(headers, rows)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,28 +1,34 @@
1
+ require "async"
2
+ require "dry-monads"
3
+
1
4
  module EndOfLife
2
5
  module Scanner
3
6
  include Helpers::Terminal
7
+ include Helpers::Text
4
8
  extend self
5
9
 
6
- def scan(options)
7
- fetch_repositories(options)
8
- .fmap { |repositories| filter_repositories_with_eol_products(repositories, **options.slice(:product, :max_eol_date)) }
9
- .fmap { |repositories| output_report(repositories, **options.slice(:product, :max_eol_date)) }
10
+ def scan(product, options)
11
+ fetch_repositories(product, options)
12
+ .fmap { |repositories| filter_repositories_with_eol_products(repositories, product, options[:max_eol_date]) }
13
+ .fmap { |repositories| output_report(repositories, product, options[:max_eol_date]) }
10
14
  .or { |error| abort "\n#{error_msg(error)}" }
11
15
  end
12
16
 
13
17
  private
14
18
 
15
- def fetch_repositories(options)
16
- with_loading_spinner("Searching repositories with #{options[:product].label}...") do |spinner|
17
- result = Repository.search(options)
19
+ def fetch_repositories(product, options)
20
+ with_loading_spinner("Searching repositories that might use #{product.label}...") do |spinner|
21
+ result = Repository.search(options.merge(product:))
18
22
  spinner.error if result.failure?
19
23
 
20
24
  result
21
25
  end
22
26
  end
23
27
 
24
- def filter_repositories_with_eol_products(repositories, product:, max_eol_date:)
25
- with_loading_spinner("Searching for EOL #{product} in your repositories...") do
28
+ def filter_repositories_with_eol_products(repositories, product, max_eol_date)
29
+ return [] if repositories.empty?
30
+
31
+ with_loading_spinner("Scanning #{pluralize(repositories.size, "repository", "repositories")} for EOL #{product.label}...") do
26
32
  Sync do
27
33
  repositories
28
34
  .map { |repo| Async { [repo, repo.using_eol?(product, at: max_eol_date)] } }.map(&:wait)
@@ -31,7 +37,7 @@ module EndOfLife
31
37
  end
32
38
  end
33
39
 
34
- def output_report(repositories, product:, max_eol_date:)
40
+ def output_report(repositories, product, max_eol_date)
35
41
  report = Report.new(product, repositories, max_eol_date)
36
42
  puts report
37
43
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EndOfLife
4
- VERSION = "0.5.1"
4
+ VERSION = "1.0.0.alpha"
5
5
  end
@@ -4,8 +4,8 @@ module EndOfLife
4
4
  @file_detectors ||= {}
5
5
  end
6
6
 
7
- def detects_from(file, &block)
8
- file_detectors[file] = block
7
+ def detects_from(*files, &block)
8
+ files.each { |file| file_detectors[file] = block }
9
9
  end
10
10
 
11
11
  def relevant_files = file_detectors.keys
@@ -0,0 +1,31 @@
1
+ require "bundler"
2
+
3
+ module EndOfLife
4
+ module VersionDetectors
5
+ module Nodejs
6
+ extend VersionDetector
7
+
8
+ detects_from ".node-version", ".nvmrc" do |file_content|
9
+ string_version = Gem::Version.new(file_content.strip.delete_prefix("v"))
10
+
11
+ Product::Release.new(product: "nodejs", version: string_version)
12
+ rescue ArgumentError
13
+ nil
14
+ end
15
+
16
+ detects_from ".tool-versions" do |file_content|
17
+ tool_versions = Parsers::ToolVersions.parse(file_content)
18
+ version = tool_versions["nodejs"] or next
19
+
20
+ Product::Release.new(product: "nodejs", version:)
21
+ end
22
+
23
+ detects_from "mise.toml" do |file_content|
24
+ tools = Parsers::MiseToml.parse(file_content) or next
25
+ version = tools["node"] or next
26
+
27
+ Product::Release.new(product: "nodejs", version:)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -30,6 +30,13 @@ module EndOfLife
30
30
 
31
31
  Product::Release.ruby(ruby_version)
32
32
  end
33
+
34
+ detects_from "mise.toml" do |file_content|
35
+ tools = Parsers::MiseToml.parse(file_content) or next
36
+ ruby_version = tools["ruby"] or next
37
+
38
+ Product::Release.ruby(ruby_version)
39
+ end
33
40
  end
34
41
  end
35
42
  end
@@ -6,8 +6,6 @@ module EndOfLife
6
6
  detector_class = "#{self}::#{camelize(product.to_s)}"
7
7
 
8
8
  const_get(detector_class)
9
- rescue NameError
10
- raise ArgumentError, "Undefined version detector for product: #{product}"
11
9
  end
12
10
 
13
11
  private
data/lib/end_of_life.rb CHANGED
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "async"
4
- require "dry-monads"
5
- require "json"
6
- require "base64"
7
- require "octokit"
8
3
  require "zeitwerk"
9
4
 
10
5
  Zeitwerk::Loader.for_gem.tap { |it|
@@ -15,6 +10,7 @@ module EndOfLife
15
10
  extend Product::Registry
16
11
  extend Helpers::Terminal
17
12
 
18
- scans_for :ruby, search_query: "language:ruby"
19
- scans_for :rails, search_query: "language:ruby"
13
+ scans_for :ruby
14
+ scans_for :rails
15
+ scans_for :nodejs, label: "Node.js"
20
16
  end
metadata CHANGED
@@ -1,14 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: end_of_life
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 1.0.0.alpha
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matheus Richard
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2025-10-24 00:00:00.000000000 Z
11
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: argument_parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.0
12
27
  - !ruby/object:Gem::Dependency
13
28
  name: async
14
29
  requirement: !ruby/object:Gem::Requirement
@@ -23,13 +38,27 @@ dependencies:
23
38
  - - ">="
24
39
  - !ruby/object:Gem::Version
25
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: base64
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
26
55
  - !ruby/object:Gem::Dependency
27
56
  name: bundler
28
57
  requirement: !ruby/object:Gem::Requirement
29
58
  requirements:
30
59
  - - ">="
31
60
  - !ruby/object:Gem::Version
32
- version: 2.3.0
61
+ version: 2.7.2
33
62
  - - "<"
34
63
  - !ruby/object:Gem::Version
35
64
  version: '3'
@@ -39,7 +68,7 @@ dependencies:
39
68
  requirements:
40
69
  - - ">="
41
70
  - !ruby/object:Gem::Version
42
- version: 2.3.0
71
+ version: 2.7.2
43
72
  - - "<"
44
73
  - !ruby/object:Gem::Version
45
74
  version: '3'
@@ -57,6 +86,20 @@ dependencies:
57
86
  - - "~>"
58
87
  - !ruby/object:Gem::Version
59
88
  version: '1.3'
89
+ - !ruby/object:Gem::Dependency
90
+ name: perfect_toml
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 0.9.0
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 0.9.0
60
103
  - !ruby/object:Gem::Dependency
61
104
  name: octokit
62
105
  requirement: !ruby/object:Gem::Requirement
@@ -71,6 +114,20 @@ dependencies:
71
114
  - - "~>"
72
115
  - !ruby/object:Gem::Version
73
116
  version: '9.0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: faraday-retry
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '2.0'
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '2.0'
74
131
  - !ruby/object:Gem::Dependency
75
132
  name: pastel
76
133
  requirement: !ruby/object:Gem::Requirement
@@ -139,7 +196,7 @@ extensions: []
139
196
  extra_rdoc_files: []
140
197
  files:
141
198
  - ".rspec"
142
- - ".tool-versions"
199
+ - ".standard.yml"
143
200
  - CHANGELOG.md
144
201
  - Gemfile
145
202
  - Gemfile.lock
@@ -147,6 +204,7 @@ files:
147
204
  - README.md
148
205
  - Rakefile
149
206
  - bin/console
207
+ - bin/end_of_life
150
208
  - bin/setup
151
209
  - demo.gif
152
210
  - end_of_life.gemspec
@@ -154,26 +212,31 @@ files:
154
212
  - exe/eol
155
213
  - lib/end_of_life.rb
156
214
  - lib/end_of_life/api.rb
215
+ - lib/end_of_life/check.rb
157
216
  - lib/end_of_life/cli.rb
217
+ - lib/end_of_life/cli/command/registry.rb
158
218
  - lib/end_of_life/helpers/silent_bundler.rb
159
219
  - lib/end_of_life/helpers/terminal.rb
220
+ - lib/end_of_life/helpers/text.rb
221
+ - lib/end_of_life/helpers/time.rb
160
222
  - lib/end_of_life/in_memory_file.rb
161
- - lib/end_of_life/options.rb
162
223
  - lib/end_of_life/parsers/gemfile.rb
163
224
  - lib/end_of_life/parsers/gemfile_lock.rb
225
+ - lib/end_of_life/parsers/mise_toml.rb
164
226
  - lib/end_of_life/parsers/ruby_version.rb
165
227
  - lib/end_of_life/parsers/tool_versions.rb
166
228
  - lib/end_of_life/product.rb
167
229
  - lib/end_of_life/product/registry.rb
168
230
  - lib/end_of_life/product/release.rb
169
- - lib/end_of_life/report.rb
170
231
  - lib/end_of_life/repository.rb
171
232
  - lib/end_of_life/repository/search.rb
172
233
  - lib/end_of_life/repository/search/query.rb
173
234
  - lib/end_of_life/scanner.rb
235
+ - lib/end_of_life/scanner/report.rb
174
236
  - lib/end_of_life/version.rb
175
237
  - lib/end_of_life/version_detector.rb
176
238
  - lib/end_of_life/version_detectors.rb
239
+ - lib/end_of_life/version_detectors/nodejs.rb
177
240
  - lib/end_of_life/version_detectors/rails.rb
178
241
  - lib/end_of_life/version_detectors/ruby.rb
179
242
  homepage: https://github.com/MatheusRich/end_of_life
@@ -183,6 +246,7 @@ metadata:
183
246
  homepage_uri: https://github.com/MatheusRich/end_of_life
184
247
  source_code_uri: https://github.com/MatheusRich/end_of_life
185
248
  changelog_uri: https://github.com/MatheusRich/end_of_life/blob/main/CHANGELOG.md
249
+ post_install_message:
186
250
  rdoc_options: []
187
251
  require_paths:
188
252
  - lib
@@ -193,11 +257,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
193
257
  version: 3.2.0
194
258
  required_rubygems_version: !ruby/object:Gem::Requirement
195
259
  requirements:
196
- - - ">="
260
+ - - ">"
197
261
  - !ruby/object:Gem::Version
198
- version: '0'
262
+ version: 1.3.1
199
263
  requirements: []
200
- rubygems_version: 3.6.9
264
+ rubygems_version: 3.4.19
265
+ signing_key:
201
266
  specification_version: 4
202
267
  summary: Lists repositories using end-of-life software
203
268
  test_files: []
data/.tool-versions DELETED
@@ -1 +0,0 @@
1
- ruby 3.4.5
@@ -1,63 +0,0 @@
1
- require "optparse"
2
-
3
- module EndOfLife
4
- module Options
5
- def self.from(argv)
6
- options = {product: Product.find("ruby"), max_eol_date: Date.today, skip_archived: true}
7
- OptionParser.new do |parser|
8
- options[:parser] = parser
9
-
10
- parser.banner = "Usage: end_of_life [options]"
11
-
12
- product_names = EndOfLife.products.map(&:name)
13
- parser.on("-p NAME", "--product NAME", /#{product_names.join("|")}/i, "Sets the product to scan for (default: ruby). Supported products are: #{product_names.join(", ")}.") do |name|
14
- options[:product] = Product.find(name)
15
- end
16
-
17
- parser.on("--exclude=NAME,NAME2", Array, "Exclude repositories containing a certain word in its name. You can specify up to five words.") do |excludes|
18
- options[:excludes] = excludes.first(5)
19
- end
20
-
21
- parser.on("--public-only", "Searches only public repositories") do
22
- options[:visibility] = :public
23
- end
24
-
25
- parser.on("--private-only", "Searches only private repositories") do
26
- options[:visibility] = :private
27
- end
28
-
29
- parser.on("--repo=USER/REPO", "--repository=USER/REPO", "Searches a specific repository") do |repository|
30
- options[:repository] = repository
31
- end
32
-
33
- parser.on("--org=ORG,ORG2...", "--organization=ORG,ORG2", Array, "Searches within specific organizations") do |organizations|
34
- options[:organizations] = organizations
35
- end
36
-
37
- parser.on("-u NAME", "--user=NAME", "Sets the user used on the repository search") do |user|
38
- options[:user] = user
39
- end
40
-
41
- parser.on("--max-eol-days-away NUMBER", "Sets the maximum number of days away a version can be from EOL. It defaults to 0.") do |days|
42
- options[:max_eol_date] = Date.today + days.to_i.abs
43
- end
44
-
45
- parser.on("--include-archived", "Includes archived repositories on the search") do
46
- options[:skip_archived] = false
47
- end
48
-
49
- parser.on("-v", "--version", "Displays end_of_life version") do
50
- options[:command] = :version
51
- end
52
-
53
- parser.on("-h", "--help", "Displays this help") do
54
- options[:command] = :help
55
- end
56
- end.parse!(argv)
57
-
58
- options
59
- rescue OptionParser::ParseError => e
60
- {command: :print_error, error: e}
61
- end
62
- end
63
- end
@@ -1,35 +0,0 @@
1
- require "stringio"
2
-
3
- module EndOfLife
4
- class Report < Data.define(:product, :repositories, :max_eol_date)
5
- include Helpers::Terminal
6
-
7
- def to_s
8
- report = StringIO.new
9
- report.puts
10
-
11
- if repositories.empty?
12
- report.puts "No repositories using EOL #{product}."
13
- else
14
- word = (repositories.size == 1) ? "repository" : "repositories"
15
- report.puts "Found #{repositories.size} #{word} using EOL #{product} (<= #{product.latest_eol_release(at: max_eol_date)}):"
16
- report.puts end_of_life_table(repositories)
17
- end
18
-
19
- report.string
20
- end
21
-
22
- def failure? = repositories.any?
23
-
24
- private
25
-
26
- def end_of_life_table(repositories)
27
- headers = ["", "Repository", "#{product} version"]
28
- rows = repositories.map.with_index(1) do |repo, i|
29
- [i, repo.url, repo.min_release_of(product)]
30
- end
31
-
32
- table(headers, rows)
33
- end
34
- end
35
- end