stackharbinger 0.3.0 → 1.0.0

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.
@@ -18,16 +18,14 @@ module Harbinger
18
18
  cache_file = cache_file_path(product)
19
19
 
20
20
  # Return fresh cache if available
21
- if cache_fresh?(cache_file)
22
- return read_cache(cache_file)
23
- end
21
+ return read_cache(cache_file) if cache_fresh?(cache_file)
24
22
 
25
23
  # Try to fetch from API
26
24
  begin
27
25
  data = fetch_from_api(product)
28
26
  write_cache(cache_file, data)
29
27
  data
30
- rescue StandardError => e
28
+ rescue StandardError
31
29
  # Fall back to stale cache if API fails
32
30
  read_cache(cache_file) if File.exist?(cache_file)
33
31
  end
@@ -39,16 +37,27 @@ module Harbinger
39
37
 
40
38
  # Extract major.minor from version (e.g., "3.2.1" -> "3.2")
41
39
  version_parts = version.split(".")
42
- major_minor = "#{version_parts[0]}.#{version_parts[1]}"
43
40
  major = version_parts[0]
41
+ major_minor = version_parts[1] ? "#{major}.#{version_parts[1]}" : nil
44
42
 
45
- # Try major.minor first (e.g., "8.0" for MySQL, "3.2" for Ruby)
46
- entry = data.find { |item| item["cycle"] == major_minor }
47
- return entry["eol"] if entry
43
+ # Try exact major.minor first (e.g., "8.0" for MySQL, "3.2" for Ruby)
44
+ if major_minor
45
+ entry = data.find { |item| item["cycle"] == major_minor }
46
+ return entry["eol"] if entry
47
+ end
48
48
 
49
- # Fall back to major only (e.g., "16" for PostgreSQL)
49
+ # Try major only (e.g., "16" for PostgreSQL)
50
50
  entry = data.find { |item| item["cycle"] == major }
51
- entry ? entry["eol"] : nil
51
+ return entry["eol"] if entry
52
+
53
+ # For major-only versions, find the latest minor version in that major series
54
+ # (e.g., version "7" should match "7.4" which is the latest 7.x)
55
+ matching_entries = data.select { |item| item["cycle"].to_s.start_with?("#{major}.") }
56
+ return nil if matching_entries.empty?
57
+
58
+ # Sort by cycle version and get the latest (highest minor version)
59
+ latest = matching_entries.max_by { |item| item["cycle"].to_s.split(".").map(&:to_i) }
60
+ latest ? latest["eol"] : nil
52
61
  end
53
62
 
54
63
  private
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "harbinger/eol_fetcher"
5
+
6
+ module Harbinger
7
+ module Exporters
8
+ # Base class for exporters that transform project data into various formats
9
+ class BaseExporter
10
+ COMPONENTS = %w[ruby rails postgres mysql redis mongo python nodejs rust].freeze
11
+ PRODUCT_NAMES = {
12
+ "ruby" => "ruby",
13
+ "rails" => "rails",
14
+ "postgres" => "postgresql",
15
+ "mysql" => "mysql",
16
+ "redis" => "redis",
17
+ "mongo" => "mongodb",
18
+ "python" => "python",
19
+ "nodejs" => "nodejs",
20
+ "rust" => "rust"
21
+ }.freeze
22
+
23
+ def initialize(projects, fetcher: nil)
24
+ @projects = projects
25
+ @fetcher = fetcher || EolFetcher.new
26
+ end
27
+
28
+ def export
29
+ raise NotImplementedError, "Subclasses must implement #export"
30
+ end
31
+
32
+ protected
33
+
34
+ def build_export_data
35
+ @projects.filter_map do |name, data|
36
+ components = build_components(data)
37
+ next if components.empty?
38
+
39
+ {
40
+ name: name,
41
+ path: data["path"],
42
+ components: components,
43
+ overall_status: determine_overall_status(components)
44
+ }
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def build_components(data)
51
+ COMPONENTS.filter_map do |component|
52
+ version = data[component]
53
+ next if version.nil? || version.empty?
54
+ next if version.include?("gem")
55
+
56
+ product = PRODUCT_NAMES[component]
57
+ eol_date = @fetcher.eol_date_for(product, version)
58
+ days = eol_date ? days_until(eol_date) : nil
59
+ status = days ? calculate_status(days) : "unknown"
60
+
61
+ {
62
+ name: component,
63
+ version: version,
64
+ eol_date: eol_date,
65
+ days_remaining: days,
66
+ status: status
67
+ }
68
+ end
69
+ end
70
+
71
+ def calculate_status(days)
72
+ if days.negative?
73
+ "eol"
74
+ elsif days < 180
75
+ "warning"
76
+ else
77
+ "safe"
78
+ end
79
+ end
80
+
81
+ def determine_overall_status(components)
82
+ statuses = components.map { |c| c[:status] }
83
+ return "unknown" if statuses.empty? || statuses.all? { |s| s == "unknown" }
84
+
85
+ if statuses.include?("eol")
86
+ "eol"
87
+ elsif statuses.include?("warning")
88
+ "warning"
89
+ else
90
+ "safe"
91
+ end
92
+ end
93
+
94
+ def days_until(date_string)
95
+ eol_date = Date.parse(date_string)
96
+ (eol_date - Date.today).to_i
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require_relative "base_exporter"
5
+
6
+ module Harbinger
7
+ module Exporters
8
+ # Exports project EOL data to CSV format
9
+ class CsvExporter < BaseExporter
10
+ HEADERS = %w[project path component version eol_date days_remaining status overall_status].freeze
11
+
12
+ def export
13
+ projects = build_export_data
14
+
15
+ CSV.generate do |csv|
16
+ csv << HEADERS
17
+
18
+ projects.each do |project|
19
+ project[:components].each do |component|
20
+ csv << [
21
+ project[:name],
22
+ project[:path],
23
+ component[:name],
24
+ component[:version],
25
+ component[:eol_date],
26
+ component[:days_remaining],
27
+ component[:status],
28
+ project[:overall_status]
29
+ ]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "base_exporter"
5
+
6
+ module Harbinger
7
+ module Exporters
8
+ # Exports project EOL data to JSON format
9
+ class JsonExporter < BaseExporter
10
+ def export
11
+ projects = build_export_data
12
+
13
+ {
14
+ generated_at: Time.now.iso8601,
15
+ project_count: projects.size,
16
+ projects: projects
17
+ }.to_json
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Harbinger
4
- VERSION = "0.3.0"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stackharbinger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rich Dabrowski
@@ -96,13 +96,23 @@ files:
96
96
  - exe/harbinger
97
97
  - lib/harbinger.rb
98
98
  - lib/harbinger/analyzers/database_detector.rb
99
+ - lib/harbinger/analyzers/docker_compose_detector.rb
100
+ - lib/harbinger/analyzers/go_detector.rb
101
+ - lib/harbinger/analyzers/mongo_detector.rb
99
102
  - lib/harbinger/analyzers/mysql_detector.rb
103
+ - lib/harbinger/analyzers/node_detector.rb
100
104
  - lib/harbinger/analyzers/postgres_detector.rb
105
+ - lib/harbinger/analyzers/python_detector.rb
101
106
  - lib/harbinger/analyzers/rails_analyzer.rb
107
+ - lib/harbinger/analyzers/redis_detector.rb
102
108
  - lib/harbinger/analyzers/ruby_detector.rb
109
+ - lib/harbinger/analyzers/rust_detector.rb
103
110
  - lib/harbinger/cli.rb
104
111
  - lib/harbinger/config_manager.rb
105
112
  - lib/harbinger/eol_fetcher.rb
113
+ - lib/harbinger/exporters/base_exporter.rb
114
+ - lib/harbinger/exporters/csv_exporter.rb
115
+ - lib/harbinger/exporters/json_exporter.rb
106
116
  - lib/harbinger/version.rb
107
117
  - sig/harbinger.rbs
108
118
  homepage: https://stackharbinger.com