stackharbinger 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9932e2c3197ac2e3a514ac213380f97c336f85c8c302745db6754f70f7c1a79
4
- data.tar.gz: 11f4b85563611e4921eba0666f29181bb26560d19ddb65dd0673e331276dd7bd
3
+ metadata.gz: 663f135c9d728ba59e2f33d33ddd73e3be1b37143c7533db0534db995301aa8e
4
+ data.tar.gz: de00d8ba69996d960b57bc96d49c79da0f9221a90ac870805fd9f5aad16b506f
5
5
  SHA512:
6
- metadata.gz: e0271ed33509bcf4fa3bce6a9f7b214753107131d3dfddbc248d68d5aea7cdb0b6a11eee4f4c2367f3060f1f1f7a7b81bca27b07e0f67a427c75eb0b0cd0efd5
7
- data.tar.gz: 71178783539fdf85565e517bad7ebc983f188ff13bfa179d77d4fc6dcea953308c8c93ccb7fe22441ea8af51d00e4a437708a2769a345f04667e4de6768f16fb
6
+ metadata.gz: 2505777f55661fe67224a94351e8eb00d56cb2ef834c19a86f00e9be6fd45cf4aea5abb872242750c5f4dc3608e4707e0ecb8899538407d348ff7fdec7254a3c
7
+ data.tar.gz: 1ee3dcbca6a3cd75ede6a720fdb50bc8969a070c4bbb0c8a17cb4abacc0b9cb4536dd78fe0b78cb7609d56e15fc5affdcebac1ec5a906911cd3e323341b882bf
data/CHANGELOG.md CHANGED
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-01-18
11
+
12
+ ### Added
13
+ - **PostgreSQL detection**: Detects PostgreSQL versions from Rails projects
14
+ - Checks `config/database.yml` for `adapter: postgresql`
15
+ - Runs `psql --version` for local databases
16
+ - Falls back to `pg` gem version from `Gemfile.lock`
17
+ - Skips shell commands for remote databases (AWS RDS, etc.) to avoid client vs server version mismatch
18
+ - **MySQL detection**: Detects MySQL versions from Rails projects
19
+ - Supports both `mysql2` and `trilogy` adapters (Rails 7.1+)
20
+ - Runs `mysql --version` or `mysqld --version` for local databases
21
+ - Falls back to gem version from `Gemfile.lock`
22
+ - Smart remote database detection
23
+ - **Rescan command**: `harbinger rescan` to bulk update all tracked projects
24
+ - Updates all projects with latest detected versions
25
+ - Automatically removes projects with missing directories
26
+ - `--verbose` flag for detailed output
27
+ - Progress counter shows scan status
28
+ - **Enhanced dashboard**: PostgreSQL and MySQL columns in `harbinger show`
29
+ - Database versions displayed in table
30
+ - Database EOL dates included in status calculation
31
+ - Dashboard prioritizes database EOL issues
32
+ - **Database EOL tracking**: Fetches EOL data for PostgreSQL and MySQL
33
+ - `harbinger update` now fetches database EOL data
34
+ - Supports major-only version cycles (PostgreSQL) and major.minor cycles (MySQL)
35
+ - **Multi-database support**: Handles Rails 6+ multi-database configurations
36
+
37
+ ### Changed
38
+ - `EolFetcher` now supports major-only version matching (PostgreSQL uses "16", not "16.11")
39
+ - Scan output includes database versions with aligned formatting
40
+ - Status calculation includes database EOL dates
41
+
42
+ ### Technical
43
+ - Added `DatabaseDetector` abstract base class for database detection
44
+ - Added `PostgresDetector` with local/remote detection (116 total tests passing)
45
+ - Added `MysqlDetector` with trilogy and mysql2 support
46
+ - Test fixtures for various database.yml configurations
47
+ - Remote database detection to avoid shell command mismatches
48
+ - Support for single and multi-database Rails configurations
49
+
10
50
  ## [0.2.0] - 2026-01-18
11
51
 
12
52
  ### Added
data/README.md CHANGED
@@ -2,20 +2,30 @@
2
2
 
3
3
  **Track End-of-Life dates for your tech stack and stay ahead of deprecations.**
4
4
 
5
- Harbinger is a CLI tool that scans your Ruby and Rails projects, detects versions, and warns you about upcoming EOL (End-of-Life) dates. Never get caught off-guard by unsupported dependencies again.
5
+ Harbinger is a CLI tool that scans your Ruby, Rails, PostgreSQL, and MySQL versions, and warns you about upcoming EOL (End-of-Life) dates. Never get caught off-guard by unsupported dependencies again.
6
6
 
7
7
  ## Features
8
8
 
9
- - 🔍 **Auto-detects versions** from `.ruby-version`, `Gemfile`, and `Gemfile.lock`
9
+ - 🔍 **Auto-detects versions** from `.ruby-version`, `Gemfile`, `Gemfile.lock`, and `config/database.yml`
10
+ - 🐘 **Database detection** for PostgreSQL and MySQL (mysql2/trilogy adapters)
10
11
  - 📅 **Fetches EOL data** from [endoflife.date](https://endoflife.date)
11
12
  - 🎨 **Color-coded warnings** (red: already EOL, yellow: <6 months, green: safe)
12
13
  - ⚡ **Smart caching** (24-hour cache, works offline after first fetch)
13
14
  - 📊 **Track multiple projects** with `--save` and view dashboard with `harbinger show`
14
- - 🔄 **Bulk scanning** with `--recursive` flag to scan entire directories
15
+ - 🔄 **Bulk operations** with `--recursive` scan and `rescan` command
15
16
  - 🚀 **Zero configuration** - just run `harbinger scan`
16
17
 
17
18
  ## Installation
18
19
 
20
+ ### Homebrew (macOS)
21
+
22
+ ```bash
23
+ brew tap RichD/harbinger
24
+ brew install stackharbinger
25
+ ```
26
+
27
+ ### RubyGems
28
+
19
29
  ```bash
20
30
  gem install stackharbinger
21
31
  ```
@@ -26,7 +36,7 @@ Or add to your Gemfile:
26
36
  gem 'stackharbinger'
27
37
  ```
28
38
 
29
- The command is still `harbinger` (shorter to type).
39
+ The command is `harbinger` (shorter to type).
30
40
 
31
41
  ## Usage
32
42
 
@@ -52,8 +62,9 @@ harbinger scan --path ~/Projects --recursive --save
52
62
  Scanning /Users/you/Projects/my-app...
53
63
 
54
64
  Detected versions:
55
- Ruby: 3.2.0
56
- Rails: 7.0.8
65
+ Ruby: 3.2.0
66
+ Rails: 7.0.8
67
+ PostgreSQL: 16.11
57
68
 
58
69
  Fetching EOL data...
59
70
 
@@ -64,6 +75,10 @@ Ruby 3.2.0:
64
75
  Rails 7.0.8:
65
76
  EOL Date: 2025-06-01
66
77
  Status: ALREADY EOL (474 days ago)
78
+
79
+ PostgreSQL 16.11:
80
+ EOL Date: 2028-11-09
81
+ Status: 1026 days remaining
67
82
  ```
68
83
 
69
84
  ### View tracked projects
@@ -71,6 +86,31 @@ Rails 7.0.8:
71
86
  ```bash
72
87
  # Show dashboard of all tracked projects
73
88
  harbinger show
89
+
90
+ # Filter to specific project(s) by name or path
91
+ harbinger show budget
92
+ harbinger show job
93
+
94
+ # Show project paths with verbose mode
95
+ harbinger show -v
96
+ harbinger show job --verbose
97
+ ```
98
+
99
+ ### Export data
100
+
101
+ ```bash
102
+ # Export to JSON (stdout)
103
+ harbinger show --format json
104
+
105
+ # Export to CSV (stdout)
106
+ harbinger show --format csv
107
+
108
+ # Save to file
109
+ harbinger show --format json -o report.json
110
+ harbinger show --format csv --output eol-report.csv
111
+
112
+ # Export filtered projects
113
+ harbinger show myproject --format json
74
114
  ```
75
115
 
76
116
  **Example output:**
@@ -78,14 +118,31 @@ harbinger show
78
118
  ```
79
119
  Tracked Projects (10)
80
120
  ================================================================================
81
- ┌───────────────────┬───────┬──────────┬─────────────┐
82
- │ Project │ Ruby │ Rails │ Status │
83
- ├───────────────────┼───────┼──────────┼─────────────┤
84
- │ ledger │ 3.3.0 │ 6.1.7.10 │ ✗ Rails EOL │
85
- │ option_tracker │ 3.3.0 │ 7.0.8.7 │ ✗ Rails EOL │
86
- │ CarCal │ - │ 8.0.2 │ ✓ Current │
87
- │ job_tracker │ 3.3.0 │ 8.0.4 │ ✓ Current │
88
- └───────────────────┴───────┴──────────┴─────────────┘
121
+ ┌───────────────────┬───────┬──────────┬────────────┬───────┬─────────────┐
122
+ │ Project │ Ruby │ Rails │ PostgreSQL │ MySQL │ Status │
123
+ ├───────────────────┼───────┼──────────┼────────────┼───────┼─────────────┤
124
+ │ ledger │ 3.3.0 │ 6.1.7.10 │ - │ - │ ✗ Rails EOL │
125
+ │ option_tracker │ 3.3.0 │ 7.0.8.7 │ - │ - │ ✗ Rails EOL │
126
+ │ CarCal │ - │ 8.0.2 │ - │ - │ ✓ Current │
127
+ │ job_tracker │ 3.3.0 │ 8.0.4 │ 16.11 │ - │ ✓ Current │
128
+ └───────────────────┴───────┴──────────┴────────────┴───────┴─────────────┘
129
+ ```
130
+
131
+ ### Re-scan all tracked projects
132
+
133
+ ```bash
134
+ # Update all tracked projects with latest versions
135
+ harbinger rescan
136
+
137
+ # Show detailed output for each project
138
+ harbinger rescan --verbose
139
+ ```
140
+
141
+ ### Remove a project
142
+
143
+ ```bash
144
+ # Remove a project from tracking
145
+ harbinger remove my-project
89
146
  ```
90
147
 
91
148
  ### Update EOL data
@@ -106,6 +163,8 @@ harbinger version
106
163
  1. **Detection**: Harbinger looks for version info in your project:
107
164
  - Ruby: `.ruby-version`, `Gemfile` (`ruby "x.x.x"`), `Gemfile.lock` (RUBY VERSION)
108
165
  - Rails: `Gemfile.lock` (rails gem)
166
+ - PostgreSQL: `config/database.yml` (adapter check) + `psql --version` or `pg` gem
167
+ - MySQL: `config/database.yml` (mysql2/trilogy adapter) + `mysql --version` or gem version
109
168
 
110
169
  2. **EOL Data**: Fetches official EOL dates from [endoflife.date](https://endoflife.date) API
111
170
 
@@ -130,6 +189,22 @@ Ruby: Present (version not specified - add .ruby-version or ruby declaration in
130
189
 
131
190
  Parses `Gemfile.lock` for the rails gem version.
132
191
 
192
+ ### PostgreSQL Detection
193
+
194
+ 1. Checks `config/database.yml` for `adapter: postgresql`
195
+ 2. Tries `psql --version` for local databases (skips for remote hosts)
196
+ 3. Falls back to `pg` gem version from `Gemfile.lock`
197
+
198
+ **Note**: For remote databases (AWS RDS, etc.), shows gem version since shell command would give local client version, not server version.
199
+
200
+ ### MySQL Detection
201
+
202
+ 1. Checks `config/database.yml` for `adapter: mysql2` or `adapter: trilogy`
203
+ 2. Tries `mysql --version` or `mysqld --version` for local databases
204
+ 3. Falls back to `mysql2` or `trilogy` gem version from `Gemfile.lock`
205
+
206
+ **Supported adapters**: `mysql2` (traditional) and `trilogy` (Rails 7.1+)
207
+
133
208
  ## Requirements
134
209
 
135
210
  - Ruby >= 3.1.0
@@ -154,23 +229,24 @@ bundle exec exe/harbinger scan .
154
229
 
155
230
  ## Roadmap
156
231
 
157
- ### V0.2.0 - Current
158
- - ✅ Dashboard: `harbinger show` to see all tracked projects
159
- - ✅ Config management: Save and track multiple projects with `--save`
160
- - ✅ Recursive scanning: `--recursive` flag to scan multiple projects at once
161
- - ✅ Enhanced project tracking with YAML config
232
+ ### V0.4.0 - Current
233
+ - ✅ Export reports to JSON/CSV
234
+ - ✅ Docker Compose database version detection
235
+ - ✅ Redis version detection
236
+ - ✅ MongoDB version detection
162
237
 
163
- ### V0.3.0 - Planned
164
- - 🐘 PostgreSQL version detection
165
- - 🗄️ MySQL version detection
166
- - 🔄 Rescan command to update all tracked projects
167
- - 📋 Export reports to JSON/CSV
238
+ ### V0.3.0
239
+ - PostgreSQL version detection with local/remote database handling
240
+ - MySQL version detection (mysql2 and trilogy adapters)
241
+ - Rescan command to update all tracked projects
242
+ - Enhanced dashboard with database columns
243
+ - ✅ EOL tracking for PostgreSQL and MySQL
168
244
 
169
245
  ### V1.0 - Future
170
246
  - 🐍 Python support (pyproject.toml, requirements.txt)
171
247
  - 📦 Node.js support (package.json, .nvmrc)
172
248
  - 🦀 Rust support (Cargo.toml)
173
- - 🏠 Homebrew distribution: `brew install harbinger`
249
+ - 🐘 Go support (go.mod)
174
250
 
175
251
  ### V2.0 - Vision
176
252
  - 🤖 AI-powered upgrade summaries
data/docs/index.html CHANGED
@@ -17,11 +17,12 @@
17
17
  <div class="max-w-6xl mx-auto px-4 py-20">
18
18
  <h1 class="text-5xl md:text-7xl font-black mb-6">Harbinger</h1>
19
19
  <p class="text-2xl md:text-3xl mb-8 text-blue-100">Track End-of-Life dates for your tech stack</p>
20
- <p class="text-xl mb-12 text-blue-100 max-w-2xl">Never get caught off-guard by unsupported dependencies. Harbinger scans your Ruby and Rails projects and warns you before support ends.</p>
20
+ <p class="text-xl mb-12 text-blue-100 max-w-2xl">Never get caught off-guard by unsupported dependencies. Harbinger scans your Ruby, Rails, PostgreSQL, and MySQL versions and warns you before support ends.</p>
21
21
 
22
22
  <div class="flex flex-col sm:flex-row gap-4">
23
23
  <div class="bg-gray-900 rounded-lg p-4 font-mono text-sm">
24
- <span class="text-gray-400">$</span> <span class="text-green-400">gem install stackharbinger</span>
24
+ <div class="mb-2"><span class="text-gray-400">$</span> <span class="text-green-400">brew tap RichD/harbinger</span></div>
25
+ <div><span class="text-gray-400">$</span> <span class="text-green-400">brew install stackharbinger</span></div>
25
26
  </div>
26
27
  <a href="https://github.com/RichD/harbinger" class="inline-block bg-white text-blue-600 px-8 py-4 rounded-lg font-semibold hover:bg-blue-50 transition text-center">
27
28
  View on GitHub →
@@ -37,7 +38,7 @@
37
38
  <div class="bg-white p-6 rounded-lg shadow-sm">
38
39
  <div class="text-4xl mb-4">🔍</div>
39
40
  <h3 class="text-xl font-semibold mb-2">Auto-Detection</h3>
40
- <p class="text-gray-600">Automatically detects Ruby and Rails versions from .ruby-version, Gemfile, and Gemfile.lock</p>
41
+ <p class="text-gray-600">Detects Ruby, Rails, PostgreSQL, and MySQL versions from project files and database configs</p>
41
42
  </div>
42
43
  <div class="bg-white p-6 rounded-lg shadow-sm">
43
44
  <div class="text-4xl mb-4">📅</div>
@@ -59,10 +60,20 @@
59
60
  <h3 class="text-xl font-semibold mb-2">Works Offline</h3>
60
61
  <p class="text-gray-600">Smart caching means it works offline after initial data fetch</p>
61
62
  </div>
63
+ <div class="bg-white p-6 rounded-lg shadow-sm">
64
+ <div class="text-4xl mb-4">📊</div>
65
+ <h3 class="text-xl font-semibold mb-2">Smart Dashboard</h3>
66
+ <p class="text-gray-600">Dynamic columns show only relevant data - columns and projects without matches are hidden automatically</p>
67
+ </div>
68
+ <div class="bg-white p-6 rounded-lg shadow-sm">
69
+ <div class="text-4xl mb-4">🔄</div>
70
+ <h3 class="text-xl font-semibold mb-2">Bulk Scanning</h3>
71
+ <p class="text-gray-600">Recursively scan entire directories to find and track all your projects</p>
72
+ </div>
62
73
  <div class="bg-white p-6 rounded-lg shadow-sm">
63
74
  <div class="text-4xl mb-4">🧪</div>
64
75
  <h3 class="text-xl font-semibold mb-2">Well Tested</h3>
65
- <p class="text-gray-600">36 RSpec tests with 100% pass rate, built with TDD</p>
76
+ <p class="text-gray-600">116 RSpec tests with 100% pass rate, built with TDD</p>
66
77
  </div>
67
78
  </div>
68
79
  </div>
@@ -73,7 +84,11 @@
73
84
  <h2 class="text-3xl font-bold mb-8">Quick Start</h2>
74
85
 
75
86
  <div class="bg-gray-900 rounded-lg p-6 mb-8 font-mono text-sm overflow-x-auto">
76
- <div class="text-gray-400"># Install</div>
87
+ <div class="text-gray-400"># Install via Homebrew</div>
88
+ <div class="mb-1"><span class="text-gray-400">$</span> <span class="text-green-400">brew tap RichD/harbinger</span></div>
89
+ <div class="mb-4"><span class="text-gray-400">$</span> <span class="text-green-400">brew install stackharbinger</span></div>
90
+
91
+ <div class="text-gray-400"># Or via RubyGems</div>
77
92
  <div class="mb-4"><span class="text-gray-400">$</span> <span class="text-green-400">gem install stackharbinger</span></div>
78
93
 
79
94
  <div class="text-gray-400"># Scan your project</div>
@@ -96,7 +111,14 @@
96
111
  <div class="prose max-w-none">
97
112
  <h3 class="text-2xl font-semibold mb-4">Commands</h3>
98
113
  <ul class="space-y-3 text-gray-700">
99
- <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan [PATH]</code> - Scan a project and show EOL status</li>
114
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --path [PATH]</code> - Scan a project and show EOL status</li>
115
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --save</code> - Save project for tracking</li>
116
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --recursive</code> - Scan all projects in a directory</li>
117
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger show [PROJECT] [-v]</code> - View dashboard (filter by name/path, -v shows paths)</li>
118
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger show --format json|csv</code> - Export data to JSON or CSV</li>
119
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger show --format json -o report.json</code> - Save export to file</li>
120
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger rescan</code> - Re-scan all tracked projects and update versions</li>
121
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger remove PROJECT</code> - Remove a project from tracking</li>
100
122
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger update</code> - Force refresh EOL data from API</li>
101
123
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger version</code> - Show harbinger version</li>
102
124
  </ul>
@@ -109,21 +131,21 @@
109
131
  <h2 class="text-3xl font-bold mb-8 text-center">Roadmap</h2>
110
132
  <div class="grid md:grid-cols-3 gap-8">
111
133
  <div>
112
- <h3 class="text-xl font-semibold mb-4 text-green-600">✅ V0.1.0 (Current)</h3>
134
+ <h3 class="text-xl font-semibold mb-4 text-green-600">✅ V0.4.0 (Current)</h3>
113
135
  <ul class="space-y-2 text-gray-600">
114
- <li>• Ruby & Rails detection</li>
115
- <li>• EOL data fetching</li>
116
- <li>• CLI with color output</li>
117
- <li>• Smart caching</li>
136
+ <li>• Export to JSON/CSV</li>
137
+ <li>• Docker Compose detection</li>
138
+ <li>• Redis detection</li>
139
+ <li>• MongoDB detection</li>
118
140
  </ul>
119
141
  </div>
120
142
  <div>
121
- <h3 class="text-xl font-semibold mb-4 text-blue-600">📋 V0.2.0 (Planned)</h3>
143
+ <h3 class="text-xl font-semibold mb-4 text-blue-600">📋 V0.3.0</h3>
122
144
  <ul class="space-y-2 text-gray-600">
123
- <li>• Dashboard view</li>
124
- <li>• Config management</li>
125
145
  <li>• PostgreSQL detection</li>
126
146
  <li>• MySQL detection</li>
147
+ <li>• Rescan command</li>
148
+ <li>• Smart dashboard filtering</li>
127
149
  </ul>
128
150
  </div>
129
151
  <div>
@@ -131,8 +153,8 @@
131
153
  <ul class="space-y-2 text-gray-600">
132
154
  <li>• Python support</li>
133
155
  <li>• Node.js support</li>
134
- <li>• Go & Rust support</li>
135
- <li>• Homebrew distribution</li>
156
+ <li>• Go support</li>
157
+ <li>• Rust support</li>
136
158
  </ul>
137
159
  </div>
138
160
  </div>
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require "yaml"
5
+
6
+ module Harbinger
7
+ module Analyzers
8
+ # Abstract base class for database version detection in Rails projects
9
+ # Provides common functionality for detecting database versions from Rails projects
10
+ class DatabaseDetector
11
+ attr_reader :project_path
12
+
13
+ def initialize(project_path)
14
+ @project_path = project_path
15
+ end
16
+
17
+ # Main detection method - returns version string or nil
18
+ def detect
19
+ return nil unless database_detected?
20
+
21
+ # Try docker-compose.yml first (most accurate for Docker-based projects)
22
+ version = detect_from_docker_compose
23
+ return version if version
24
+
25
+ # Try shell command (actual database version for non-Docker setups)
26
+ version = detect_from_shell
27
+ return version if version
28
+
29
+ # Fallback to gem version from Gemfile.lock
30
+ detect_from_gemfile_lock
31
+ end
32
+
33
+ # Check if database.yml indicates this database is used
34
+ def database_detected?
35
+ return false unless database_yml_exists?
36
+
37
+ config = parse_database_yml
38
+ return false unless config
39
+
40
+ # Check production or default section
41
+ section = config["production"] || config["default"] || config[config.keys.first]
42
+ return false unless section
43
+
44
+ adapter = extract_adapter_from_section(section)
45
+ return false unless adapter
46
+
47
+ Array(adapter_name).any? { |name| adapter == name }
48
+ end
49
+
50
+ protected
51
+
52
+ # Extract adapter from database config section
53
+ # Handles both single-database and multi-database (Rails 6+) configurations
54
+ def extract_adapter_from_section(section)
55
+ # Single database: { "adapter" => "postgresql", ... }
56
+ return section["adapter"] if section["adapter"]
57
+
58
+ # Multi-database: { "primary" => { "adapter" => "postgresql", ... }, "cache" => { ... } }
59
+ # Check primary first, then fall back to first nested config
60
+ nested_config = section["primary"] || section.values.find { |v| v.is_a?(Hash) && v["adapter"] }
61
+ nested_config["adapter"] if nested_config
62
+ end
63
+
64
+ # Abstract method - must be implemented by subclasses
65
+ # Returns the adapter name(s) to look for in database.yml
66
+ def adapter_name
67
+ raise NotImplementedError, "Subclasses must implement adapter_name"
68
+ end
69
+
70
+ # Abstract method - must be implemented by subclasses
71
+ # Detects version from shell command (e.g., psql --version)
72
+ def detect_from_shell
73
+ raise NotImplementedError, "Subclasses must implement detect_from_shell"
74
+ end
75
+
76
+ # Optional method - subclasses can override to detect from docker-compose.yml
77
+ def detect_from_docker_compose
78
+ nil
79
+ end
80
+
81
+ # Abstract method - must be implemented by subclasses
82
+ # Detects version from Gemfile.lock gem version
83
+ def detect_from_gemfile_lock
84
+ raise NotImplementedError, "Subclasses must implement detect_from_gemfile_lock"
85
+ end
86
+
87
+ # Read and parse config/database.yml
88
+ def parse_database_yml
89
+ database_yml_path = File.join(project_path, "config", "database.yml")
90
+ return nil unless File.exist?(database_yml_path)
91
+
92
+ content = File.read(database_yml_path)
93
+ YAML.safe_load(content, aliases: true)
94
+ rescue StandardError
95
+ nil
96
+ end
97
+
98
+ # Check if database.yml exists
99
+ def database_yml_exists?
100
+ File.exist?(File.join(project_path, "config", "database.yml"))
101
+ end
102
+
103
+ # Read and parse Gemfile.lock
104
+ def parse_gemfile_lock
105
+ gemfile_lock_path = File.join(project_path, "Gemfile.lock")
106
+ return nil unless File.exist?(gemfile_lock_path)
107
+
108
+ File.read(gemfile_lock_path)
109
+ rescue StandardError
110
+ nil
111
+ end
112
+
113
+ # Extract gem version from Gemfile.lock content
114
+ def extract_gem_version(gemfile_lock_content, gem_name)
115
+ return nil unless gemfile_lock_content
116
+
117
+ # Match pattern like: " pg (1.5.4)"
118
+ match = gemfile_lock_content.match(/^\s{4}#{Regexp.escape(gem_name)}\s+\(([^)]+)\)/)
119
+ match[1] if match
120
+ end
121
+
122
+ # Execute shell command safely
123
+ def execute_command(command)
124
+ output = `#{command} 2>&1`.strip
125
+ return nil unless $CHILD_STATUS.success?
126
+
127
+ output
128
+ rescue StandardError
129
+ nil
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Harbinger
6
+ module Analyzers
7
+ # Detects versions from docker-compose.yml and Dockerfile
8
+ class DockerComposeDetector
9
+ attr_reader :project_path
10
+
11
+ def initialize(project_path)
12
+ @project_path = project_path
13
+ end
14
+
15
+ # Extract version from a Docker image in docker-compose.yml
16
+ # e.g., "postgres:16-alpine" => "16", "mysql:8.0" => "8.0"
17
+ def image_version(image_pattern)
18
+ compose = parse_docker_compose
19
+ return nil unless compose
20
+
21
+ services = compose["services"]
22
+ return nil unless services
23
+
24
+ services.each_value do |service|
25
+ image = service["image"]
26
+ next unless image
27
+
28
+ if image.match?(/^#{image_pattern}[:\d]/)
29
+ version = extract_version_from_image(image)
30
+ return version if version
31
+ end
32
+ end
33
+
34
+ nil
35
+ end
36
+
37
+ # Extract Ruby version from Dockerfile
38
+ # e.g., "FROM ruby:3.4.7-slim" => "3.4.7"
39
+ def ruby_version_from_dockerfile
40
+ dockerfile = read_dockerfile
41
+ return nil unless dockerfile
42
+
43
+ # Match patterns like:
44
+ # FROM ruby:3.4.7
45
+ # FROM ruby:3.4.7-slim
46
+ # FROM ruby:3.4.7-alpine
47
+ match = dockerfile.match(/^FROM\s+ruby:(\d+\.\d+(?:\.\d+)?)/i)
48
+ match[1] if match
49
+ end
50
+
51
+ # Extract Rails version from Dockerfile (rare, but possible)
52
+ def rails_version_from_dockerfile
53
+ dockerfile = read_dockerfile
54
+ return nil unless dockerfile
55
+
56
+ # Match patterns like:
57
+ # FROM rails:7.0
58
+ # ARG RAILS_VERSION=7.0.8
59
+ match = dockerfile.match(/^FROM\s+rails:(\d+\.\d+(?:\.\d+)?)/i)
60
+ return match[1] if match
61
+
62
+ match = dockerfile.match(/RAILS_VERSION[=:](\d+\.\d+(?:\.\d+)?)/i)
63
+ match[1] if match
64
+ end
65
+
66
+ # Check if docker-compose.yml exists
67
+ def docker_compose_exists?
68
+ docker_compose_path != nil
69
+ end
70
+
71
+ # Check if Dockerfile exists
72
+ def dockerfile_exists?
73
+ File.exist?(File.join(project_path, "Dockerfile"))
74
+ end
75
+
76
+ private
77
+
78
+ def docker_compose_path
79
+ paths = [
80
+ File.join(project_path, "docker-compose.yml"),
81
+ File.join(project_path, "docker-compose.yaml"),
82
+ File.join(project_path, "compose.yml"),
83
+ File.join(project_path, "compose.yaml")
84
+ ]
85
+ paths.find { |p| File.exist?(p) }
86
+ end
87
+
88
+ def parse_docker_compose
89
+ path = docker_compose_path
90
+ return nil unless path
91
+
92
+ YAML.safe_load(File.read(path), aliases: true)
93
+ rescue StandardError
94
+ nil
95
+ end
96
+
97
+ def read_dockerfile
98
+ path = File.join(project_path, "Dockerfile")
99
+ return nil unless File.exist?(path)
100
+
101
+ File.read(path)
102
+ rescue StandardError
103
+ nil
104
+ end
105
+
106
+ # Extract version number from Docker image tag
107
+ # "postgres:16-alpine" => "16"
108
+ # "postgres:16.2" => "16.2"
109
+ # "mysql:8.0.33" => "8.0.33"
110
+ # "redis:7-alpine" => "7"
111
+ def extract_version_from_image(image)
112
+ return nil unless image.include?(":")
113
+
114
+ tag = image.split(":").last
115
+ # Extract leading version number (digits and dots)
116
+ match = tag.match(/^(\d+(?:\.\d+)*)/)
117
+ match[1] if match
118
+ end
119
+ end
120
+ end
121
+ end