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.
- checksums.yaml +4 -4
- data/README.md +143 -35
- data/docs/index.html +70 -23
- data/lib/harbinger/analyzers/database_detector.rb +13 -3
- data/lib/harbinger/analyzers/docker_compose_detector.rb +121 -0
- data/lib/harbinger/analyzers/go_detector.rb +86 -0
- data/lib/harbinger/analyzers/mongo_detector.rb +104 -0
- data/lib/harbinger/analyzers/mysql_detector.rb +8 -1
- data/lib/harbinger/analyzers/node_detector.rb +109 -0
- data/lib/harbinger/analyzers/postgres_detector.rb +6 -0
- data/lib/harbinger/analyzers/python_detector.rb +110 -0
- data/lib/harbinger/analyzers/rails_analyzer.rb +5 -1
- data/lib/harbinger/analyzers/redis_detector.rb +98 -0
- data/lib/harbinger/analyzers/ruby_detector.rb +9 -1
- data/lib/harbinger/analyzers/rust_detector.rb +116 -0
- data/lib/harbinger/cli.rb +453 -149
- data/lib/harbinger/eol_fetcher.rb +19 -10
- data/lib/harbinger/exporters/base_exporter.rb +100 -0
- data/lib/harbinger/exporters/csv_exporter.rb +36 -0
- data/lib/harbinger/exporters/json_exporter.rb +21 -0
- data/lib/harbinger/version.rb +1 -1
- metadata +11 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Harbinger
|
|
4
|
+
module Analyzers
|
|
5
|
+
class GoDetector
|
|
6
|
+
def initialize(project_path)
|
|
7
|
+
@project_path = project_path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def detect
|
|
11
|
+
version = detect_from_go_mod ||
|
|
12
|
+
detect_from_go_work ||
|
|
13
|
+
detect_from_go_version_file ||
|
|
14
|
+
detect_from_docker_compose ||
|
|
15
|
+
detect_from_shell
|
|
16
|
+
|
|
17
|
+
normalize_version(version) if version
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def go_detected?
|
|
21
|
+
File.exist?(File.join(@project_path, "go.mod")) ||
|
|
22
|
+
File.exist?(File.join(@project_path, "go.work")) ||
|
|
23
|
+
File.exist?(File.join(@project_path, ".go-version"))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def detect_from_go_mod
|
|
29
|
+
go_mod_path = File.join(@project_path, "go.mod")
|
|
30
|
+
return nil unless File.exist?(go_mod_path)
|
|
31
|
+
|
|
32
|
+
content = File.read(go_mod_path)
|
|
33
|
+
# Match "go 1.21" or "go 1.21.0" format
|
|
34
|
+
match = content.match(/^go\s+([\d.]+)/m)
|
|
35
|
+
match[1] if match
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def detect_from_go_work
|
|
39
|
+
go_work_path = File.join(@project_path, "go.work")
|
|
40
|
+
return nil unless File.exist?(go_work_path)
|
|
41
|
+
|
|
42
|
+
content = File.read(go_work_path)
|
|
43
|
+
# Match "go 1.21" or "go 1.21.0" format
|
|
44
|
+
match = content.match(/^go\s+([\d.]+)/m)
|
|
45
|
+
match[1] if match
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def detect_from_go_version_file
|
|
49
|
+
go_version_path = File.join(@project_path, ".go-version")
|
|
50
|
+
return nil unless File.exist?(go_version_path)
|
|
51
|
+
|
|
52
|
+
version = File.read(go_version_path).strip
|
|
53
|
+
version unless version.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def detect_from_docker_compose
|
|
57
|
+
docker_compose_path = File.join(@project_path, "docker-compose.yml")
|
|
58
|
+
return nil unless File.exist?(docker_compose_path)
|
|
59
|
+
|
|
60
|
+
content = File.read(docker_compose_path)
|
|
61
|
+
# Match golang:1.21, golang:1.21.0, golang:1.21-alpine, etc.
|
|
62
|
+
match = content.match(/golang:([\d.]+)/)
|
|
63
|
+
match[1] if match
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def detect_from_shell
|
|
67
|
+
# Only try shell detection if Go project files exist
|
|
68
|
+
return nil unless go_detected?
|
|
69
|
+
|
|
70
|
+
version_output = `go version 2>/dev/null`.strip
|
|
71
|
+
return nil if version_output.empty?
|
|
72
|
+
|
|
73
|
+
# Parse "go version go1.21.0 darwin/amd64" format
|
|
74
|
+
match = version_output.match(/go version go([\d.]+)/)
|
|
75
|
+
match[1] if match
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def normalize_version(version)
|
|
81
|
+
# Remove any trailing qualifiers like -alpine, -rc1, etc.
|
|
82
|
+
version.gsub(/-(alpine|rc\d+|beta\d*).*/, "")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "docker_compose_detector"
|
|
4
|
+
|
|
5
|
+
module Harbinger
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Detects MongoDB version from projects
|
|
8
|
+
class MongoDetector
|
|
9
|
+
attr_reader :project_path
|
|
10
|
+
|
|
11
|
+
def initialize(project_path)
|
|
12
|
+
@project_path = project_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Main detection method - returns version string or nil
|
|
16
|
+
def detect
|
|
17
|
+
return nil unless mongo_detected?
|
|
18
|
+
|
|
19
|
+
# Try docker-compose.yml first
|
|
20
|
+
version = detect_from_docker_compose
|
|
21
|
+
return version if version
|
|
22
|
+
|
|
23
|
+
# Try shell command
|
|
24
|
+
version = detect_from_shell
|
|
25
|
+
return version if version
|
|
26
|
+
|
|
27
|
+
# Fallback to gem version
|
|
28
|
+
detect_from_gemfile_lock
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if MongoDB is used in this project
|
|
32
|
+
def mongo_detected?
|
|
33
|
+
gemfile_has_mongo? || docker_compose_has_mongo?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def detect_from_docker_compose
|
|
39
|
+
docker = DockerComposeDetector.new(project_path)
|
|
40
|
+
docker.image_version("mongo")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def detect_from_shell
|
|
44
|
+
# Try mongosh first (modern shell, MongoDB 5+)
|
|
45
|
+
output = `mongosh --version 2>&1`.strip
|
|
46
|
+
return output if $CHILD_STATUS.success? && output.match?(/^\d+\.\d+/)
|
|
47
|
+
|
|
48
|
+
# Try legacy mongo shell
|
|
49
|
+
output = `mongo --version 2>&1`.strip
|
|
50
|
+
if $CHILD_STATUS.success?
|
|
51
|
+
match = output.match(/MongoDB shell version v?(\d+\.\d+(?:\.\d+)?)/)
|
|
52
|
+
return match[1] if match
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Fall back to mongod (server)
|
|
56
|
+
output = `mongod --version 2>&1`.strip
|
|
57
|
+
return nil unless $CHILD_STATUS.success?
|
|
58
|
+
|
|
59
|
+
match = output.match(/db version v(\d+\.\d+(?:\.\d+)?)/)
|
|
60
|
+
match[1] if match
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def detect_from_gemfile_lock
|
|
66
|
+
content = read_gemfile_lock
|
|
67
|
+
return nil unless content
|
|
68
|
+
|
|
69
|
+
# Look for mongoid gem version first (most common for Rails)
|
|
70
|
+
match = content.match(/^\s{4}mongoid\s+\(([^)]+)\)/)
|
|
71
|
+
return "#{match[1]} (mongoid gem)" if match
|
|
72
|
+
|
|
73
|
+
# Fall back to mongo gem
|
|
74
|
+
match = content.match(/^\s{4}mongo\s+\(([^)]+)\)/)
|
|
75
|
+
return "#{match[1]} (mongo gem)" if match
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def gemfile_has_mongo?
|
|
81
|
+
content = read_gemfile_lock
|
|
82
|
+
return false unless content
|
|
83
|
+
|
|
84
|
+
content.include?("mongoid (") || content.include?("mongo (")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def docker_compose_has_mongo?
|
|
88
|
+
docker = DockerComposeDetector.new(project_path)
|
|
89
|
+
return false unless docker.docker_compose_exists?
|
|
90
|
+
|
|
91
|
+
docker.image_version("mongo") != nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def read_gemfile_lock
|
|
95
|
+
path = File.join(project_path, "Gemfile.lock")
|
|
96
|
+
return nil unless File.exist?(path)
|
|
97
|
+
|
|
98
|
+
File.read(path)
|
|
99
|
+
rescue StandardError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "database_detector"
|
|
4
|
+
require_relative "docker_compose_detector"
|
|
4
5
|
|
|
5
6
|
module Harbinger
|
|
6
7
|
module Analyzers
|
|
@@ -10,7 +11,13 @@ module Harbinger
|
|
|
10
11
|
protected
|
|
11
12
|
|
|
12
13
|
def adapter_name
|
|
13
|
-
[
|
|
14
|
+
%w[mysql2 trilogy]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def detect_from_docker_compose
|
|
18
|
+
docker = DockerComposeDetector.new(project_path)
|
|
19
|
+
# Try mysql first, then mariadb
|
|
20
|
+
docker.image_version("mysql") || docker.image_version("mariadb")
|
|
14
21
|
end
|
|
15
22
|
|
|
16
23
|
def detect_from_shell
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "docker_compose_detector"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Harbinger
|
|
7
|
+
module Analyzers
|
|
8
|
+
class NodeDetector
|
|
9
|
+
def initialize(project_path)
|
|
10
|
+
@project_path = project_path
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def detect
|
|
14
|
+
detect_from_version_files ||
|
|
15
|
+
detect_from_package_json ||
|
|
16
|
+
detect_from_docker_compose ||
|
|
17
|
+
(nodejs_detected? ? detect_from_shell : nil)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def nodejs_detected?
|
|
21
|
+
File.exist?(File.join(project_path, "package.json")) ||
|
|
22
|
+
File.exist?(File.join(project_path, "package-lock.json")) ||
|
|
23
|
+
File.exist?(File.join(project_path, ".nvmrc")) ||
|
|
24
|
+
File.exist?(File.join(project_path, ".node-version")) ||
|
|
25
|
+
File.exist?(File.join(project_path, "node_modules"))
|
|
26
|
+
rescue StandardError
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :project_path
|
|
33
|
+
|
|
34
|
+
def detect_from_version_files
|
|
35
|
+
[".nvmrc", ".node-version"].each do |filename|
|
|
36
|
+
file_path = File.join(project_path, filename)
|
|
37
|
+
next unless File.exist?(file_path)
|
|
38
|
+
|
|
39
|
+
content = File.read(file_path).strip
|
|
40
|
+
next if content.empty?
|
|
41
|
+
|
|
42
|
+
return extract_version(content)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
nil
|
|
46
|
+
rescue StandardError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def detect_from_package_json
|
|
51
|
+
file_path = File.join(project_path, "package.json")
|
|
52
|
+
return nil unless File.exist?(file_path)
|
|
53
|
+
|
|
54
|
+
content = File.read(file_path)
|
|
55
|
+
package = JSON.parse(content)
|
|
56
|
+
|
|
57
|
+
engines = package.dig("engines", "node")
|
|
58
|
+
return nil unless engines
|
|
59
|
+
|
|
60
|
+
extract_version(engines)
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def detect_from_docker_compose
|
|
66
|
+
docker = DockerComposeDetector.new(project_path)
|
|
67
|
+
docker.image_version("node")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def detect_from_shell
|
|
71
|
+
output = `node --version 2>&1`.strip
|
|
72
|
+
return nil unless $CHILD_STATUS.success?
|
|
73
|
+
|
|
74
|
+
# Parse: "v18.16.0" (note the 'v' prefix)
|
|
75
|
+
match = output.match(/^v?(\d+\.\d+(?:\.\d+)?)/)
|
|
76
|
+
match[1] if match
|
|
77
|
+
rescue StandardError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_version(version_string)
|
|
82
|
+
# Remove 'v' prefix (Node.js convention)
|
|
83
|
+
version = version_string.sub(/^v/, "")
|
|
84
|
+
|
|
85
|
+
# Strip constraint operators: >=18.0.0, ^18.0.0, ~18.0.0
|
|
86
|
+
version = version.gsub(/^[><=~^!\s]+/, "")
|
|
87
|
+
|
|
88
|
+
# Handle ranges like ">=14.0.0 <20.0.0" - extract first version
|
|
89
|
+
version = version.split(/\s+/).first if version.include?(" ")
|
|
90
|
+
|
|
91
|
+
# Handle .x suffix (e.g., "18.x" => "18")
|
|
92
|
+
version = version.sub(/\.x$/, "")
|
|
93
|
+
|
|
94
|
+
# Handle LTS names like "lts/hydrogen" (hydrogen=18, gallium=16, fermium=14)
|
|
95
|
+
if version =~ /lts\/(hydrogen|gallium|fermium)/i
|
|
96
|
+
lts_name = $1.downcase
|
|
97
|
+
return case lts_name
|
|
98
|
+
when "hydrogen" then "18"
|
|
99
|
+
when "gallium" then "16"
|
|
100
|
+
when "fermium" then "14"
|
|
101
|
+
else nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
version
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "database_detector"
|
|
4
|
+
require_relative "docker_compose_detector"
|
|
4
5
|
|
|
5
6
|
module Harbinger
|
|
6
7
|
module Analyzers
|
|
@@ -12,6 +13,11 @@ module Harbinger
|
|
|
12
13
|
"postgresql"
|
|
13
14
|
end
|
|
14
15
|
|
|
16
|
+
def detect_from_docker_compose
|
|
17
|
+
docker = DockerComposeDetector.new(project_path)
|
|
18
|
+
docker.image_version("postgres")
|
|
19
|
+
end
|
|
20
|
+
|
|
15
21
|
def detect_from_shell
|
|
16
22
|
# Skip shell command if database is remote
|
|
17
23
|
# (shell gives client version, not server version)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Harbinger
|
|
4
|
+
module Analyzers
|
|
5
|
+
class PythonDetector
|
|
6
|
+
def initialize(project_path)
|
|
7
|
+
@project_path = project_path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def detect
|
|
11
|
+
detect_from_pyproject_toml ||
|
|
12
|
+
detect_from_python_version ||
|
|
13
|
+
detect_from_pyvenv_cfg ||
|
|
14
|
+
(python_detected? ? detect_from_shell : nil)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def python_detected?
|
|
18
|
+
File.exist?(File.join(project_path, "pyproject.toml")) ||
|
|
19
|
+
File.exist?(File.join(project_path, "requirements.txt")) ||
|
|
20
|
+
File.exist?(File.join(project_path, ".python-version")) ||
|
|
21
|
+
File.exist?(File.join(project_path, "setup.py")) ||
|
|
22
|
+
File.exist?(File.join(project_path, "setup.cfg")) ||
|
|
23
|
+
venv_exists?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :project_path
|
|
29
|
+
|
|
30
|
+
def detect_from_pyproject_toml
|
|
31
|
+
file_path = File.join(project_path, "pyproject.toml")
|
|
32
|
+
return nil unless File.exist?(file_path)
|
|
33
|
+
|
|
34
|
+
content = File.read(file_path)
|
|
35
|
+
|
|
36
|
+
# Try [project] requires-python = ">=3.11"
|
|
37
|
+
match = content.match(/requires-python\s*=\s*["']([^"']+)["']/)
|
|
38
|
+
return extract_version(match[1]) if match
|
|
39
|
+
|
|
40
|
+
# Try [tool.poetry.dependencies] python = "^3.11"
|
|
41
|
+
match = content.match(/\[tool\.poetry\.dependencies\].*?python\s*=\s*["']([^"']+)["']/m)
|
|
42
|
+
return extract_version(match[1]) if match
|
|
43
|
+
|
|
44
|
+
nil
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def detect_from_python_version
|
|
50
|
+
file_path = File.join(project_path, ".python-version")
|
|
51
|
+
return nil unless File.exist?(file_path)
|
|
52
|
+
|
|
53
|
+
content = File.read(file_path).strip
|
|
54
|
+
extract_version(content)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def detect_from_pyvenv_cfg
|
|
60
|
+
["venv/pyvenv.cfg", ".venv/pyvenv.cfg"].each do |cfg_path|
|
|
61
|
+
file_path = File.join(project_path, cfg_path)
|
|
62
|
+
next unless File.exist?(file_path)
|
|
63
|
+
|
|
64
|
+
content = File.read(file_path)
|
|
65
|
+
# Parse: "version = 3.11.5"
|
|
66
|
+
match = content.match(/version\s*=\s*(\d+\.\d+(?:\.\d+)?)/)
|
|
67
|
+
return match[1] if match
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
nil
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def detect_from_shell
|
|
76
|
+
# Try python3 first (more reliable on multi-Python systems)
|
|
77
|
+
output = `python3 --version 2>&1`.strip
|
|
78
|
+
if $CHILD_STATUS.success?
|
|
79
|
+
match = output.match(/Python\s+(\d+\.\d+(?:\.\d+)?)/)
|
|
80
|
+
return match[1] if match
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Fall back to python
|
|
84
|
+
output = `python --version 2>&1`.strip
|
|
85
|
+
return nil unless $CHILD_STATUS.success?
|
|
86
|
+
|
|
87
|
+
match = output.match(/Python\s+(\d+\.\d+(?:\.\d+)?)/)
|
|
88
|
+
match[1] if match
|
|
89
|
+
rescue StandardError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def extract_version(version_string)
|
|
94
|
+
# Strip constraint operators: >=3.11, ^3.11, ~3.11, >3.11, <4.0
|
|
95
|
+
version = version_string.gsub(/^[><=~^!\s]+/, "")
|
|
96
|
+
|
|
97
|
+
# Handle ranges like ">=3.9,<4.0" - extract first version
|
|
98
|
+
version = version.split(",").first.strip if version.include?(",")
|
|
99
|
+
|
|
100
|
+
# Remove "python-" prefix if present (from .python-version)
|
|
101
|
+
version.sub(/^python-/, "")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def venv_exists?
|
|
105
|
+
File.exist?(File.join(project_path, "venv/pyvenv.cfg")) ||
|
|
106
|
+
File.exist?(File.join(project_path, ".venv/pyvenv.cfg"))
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -31,7 +31,11 @@ module Harbinger
|
|
|
31
31
|
|
|
32
32
|
content = File.read(file_path)
|
|
33
33
|
match = content.match(/^\s*rails\s+\(([^)]+)\)/)
|
|
34
|
-
|
|
34
|
+
return nil unless match
|
|
35
|
+
|
|
36
|
+
version_string = match[1]
|
|
37
|
+
# Strip version constraint operators (>=, ~>, =, etc.) and extract actual version
|
|
38
|
+
version_string.sub(/^[><=~!\s]+/, "").strip
|
|
35
39
|
rescue StandardError
|
|
36
40
|
nil
|
|
37
41
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "docker_compose_detector"
|
|
4
|
+
|
|
5
|
+
module Harbinger
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Detects Redis version from projects
|
|
8
|
+
class RedisDetector
|
|
9
|
+
attr_reader :project_path
|
|
10
|
+
|
|
11
|
+
def initialize(project_path)
|
|
12
|
+
@project_path = project_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Main detection method - returns version string or nil
|
|
16
|
+
def detect
|
|
17
|
+
return nil unless redis_detected?
|
|
18
|
+
|
|
19
|
+
# Try docker-compose.yml first
|
|
20
|
+
version = detect_from_docker_compose
|
|
21
|
+
return version if version
|
|
22
|
+
|
|
23
|
+
# Try shell command
|
|
24
|
+
version = detect_from_shell
|
|
25
|
+
return version if version
|
|
26
|
+
|
|
27
|
+
# Fallback to gem version
|
|
28
|
+
detect_from_gemfile_lock
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if Redis is used in this project
|
|
32
|
+
def redis_detected?
|
|
33
|
+
gemfile_has_redis? || docker_compose_has_redis?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def detect_from_docker_compose
|
|
39
|
+
docker = DockerComposeDetector.new(project_path)
|
|
40
|
+
docker.image_version("redis")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def detect_from_shell
|
|
44
|
+
# Try redis-cli first (more commonly available)
|
|
45
|
+
output = `redis-cli -v 2>&1`.strip
|
|
46
|
+
if $CHILD_STATUS.success?
|
|
47
|
+
# Parse: "redis-cli 7.0.5"
|
|
48
|
+
match = output.match(/redis-cli\s+(\d+\.\d+(?:\.\d+)?)/)
|
|
49
|
+
return match[1] if match
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Fall back to redis-server
|
|
53
|
+
output = `redis-server --version 2>&1`.strip
|
|
54
|
+
return nil unless $CHILD_STATUS.success?
|
|
55
|
+
|
|
56
|
+
# Parse: "Redis server v=7.2.4 sha=..."
|
|
57
|
+
match = output.match(/v=(\d+\.\d+(?:\.\d+)?)/)
|
|
58
|
+
match[1] if match
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def detect_from_gemfile_lock
|
|
64
|
+
content = read_gemfile_lock
|
|
65
|
+
return nil unless content
|
|
66
|
+
|
|
67
|
+
# Look for redis gem version
|
|
68
|
+
match = content.match(/^\s{4}redis\s+\(([^)]+)\)/)
|
|
69
|
+
return "#{match[1]} (gem)" if match
|
|
70
|
+
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def gemfile_has_redis?
|
|
75
|
+
content = read_gemfile_lock
|
|
76
|
+
return false unless content
|
|
77
|
+
|
|
78
|
+
content.include?("redis (")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def docker_compose_has_redis?
|
|
82
|
+
docker = DockerComposeDetector.new(project_path)
|
|
83
|
+
return false unless docker.docker_compose_exists?
|
|
84
|
+
|
|
85
|
+
docker.image_version("redis") != nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read_gemfile_lock
|
|
89
|
+
path = File.join(project_path, "Gemfile.lock")
|
|
90
|
+
return nil unless File.exist?(path)
|
|
91
|
+
|
|
92
|
+
File.read(path)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "docker_compose_detector"
|
|
4
|
+
|
|
3
5
|
module Harbinger
|
|
4
6
|
module Analyzers
|
|
5
7
|
class RubyDetector
|
|
@@ -10,7 +12,8 @@ module Harbinger
|
|
|
10
12
|
def detect
|
|
11
13
|
detect_from_ruby_version ||
|
|
12
14
|
detect_from_gemfile ||
|
|
13
|
-
detect_from_gemfile_lock
|
|
15
|
+
detect_from_gemfile_lock ||
|
|
16
|
+
detect_from_dockerfile
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def ruby_detected?
|
|
@@ -61,6 +64,11 @@ module Harbinger
|
|
|
61
64
|
# Remove patch level suffix (e.g., "p223")
|
|
62
65
|
version.sub(/p\d+$/, "")
|
|
63
66
|
end
|
|
67
|
+
|
|
68
|
+
def detect_from_dockerfile
|
|
69
|
+
docker = DockerComposeDetector.new(project_path)
|
|
70
|
+
docker.ruby_version_from_dockerfile
|
|
71
|
+
end
|
|
64
72
|
end
|
|
65
73
|
end
|
|
66
74
|
end
|