source_monitor 0.4.0 → 0.5.1

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/sm-host-setup/SKILL.md +3 -1
  3. data/.claude/skills/sm-upgrade/SKILL.md +102 -0
  4. data/.claude/skills/sm-upgrade/reference/upgrade-workflow.md +92 -0
  5. data/.claude/skills/sm-upgrade/reference/version-history.md +68 -0
  6. data/.gitignore +1 -0
  7. data/.vbw-planning/SHIPPED.md +35 -0
  8. data/.vbw-planning/milestones/generator-enhancements/SHIPPED.md +40 -0
  9. data/.vbw-planning/milestones/upgrade-assurance/REQUIREMENTS.md +80 -0
  10. data/.vbw-planning/milestones/upgrade-assurance/ROADMAP.md +75 -0
  11. data/.vbw-planning/milestones/upgrade-assurance/STATE.md +29 -0
  12. data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/01-VERIFICATION.md +144 -0
  13. data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01-SUMMARY.md +43 -0
  14. data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01.md +405 -0
  15. data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01-SUMMARY.md +27 -0
  16. data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01.md +303 -0
  17. data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/03-VERIFICATION.md +380 -0
  18. data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01-SUMMARY.md +36 -0
  19. data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01.md +652 -0
  20. data/CHANGELOG.md +25 -0
  21. data/CLAUDE.md +5 -3
  22. data/Gemfile.lock +4 -4
  23. data/VERSION +1 -1
  24. data/docs/upgrade.md +140 -0
  25. data/lib/source_monitor/configuration/deprecation_registry.rb +237 -0
  26. data/lib/source_monitor/configuration.rb +8 -0
  27. data/lib/source_monitor/setup/cli.rb +7 -0
  28. data/lib/source_monitor/setup/skills_installer.rb +1 -0
  29. data/lib/source_monitor/setup/upgrade_command.rb +59 -0
  30. data/lib/source_monitor/setup/verification/pending_migrations_verifier.rb +92 -0
  31. data/lib/source_monitor/setup/verification/runner.rb +1 -1
  32. data/lib/source_monitor/version.rb +1 -1
  33. data/lib/source_monitor.rb +3 -0
  34. metadata +44 -25
  35. /data/.vbw-planning/{REQUIREMENTS.md → milestones/generator-enhancements/REQUIREMENTS.md} +0 -0
  36. /data/.vbw-planning/{ROADMAP.md → milestones/generator-enhancements/ROADMAP.md} +0 -0
  37. /data/.vbw-planning/{STATE.md → milestones/generator-enhancements/STATE.md} +0 -0
  38. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/01-generator-steps/01-CONTEXT.md +0 -0
  39. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/01-generator-steps/01-VERIFICATION.md +0 -0
  40. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/01-generator-steps/PLAN-01-SUMMARY.md +0 -0
  41. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/01-generator-steps/PLAN-01.md +0 -0
  42. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/02-verification/02-VERIFICATION.md +0 -0
  43. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/02-verification/PLAN-01-SUMMARY.md +0 -0
  44. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/02-verification/PLAN-01.md +0 -0
  45. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/03-docs-alignment/03-VERIFICATION.md +0 -0
  46. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/03-docs-alignment/PLAN-01-SUMMARY.md +0 -0
  47. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/03-docs-alignment/PLAN-01.md +0 -0
  48. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/04-dashboard-ux/04-VERIFICATION.md +0 -0
  49. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/04-dashboard-ux/PLAN-01-SUMMARY.md +0 -0
  50. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/04-dashboard-ux/PLAN-01.md +0 -0
  51. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/05-active-storage-images/05-VERIFICATION.md +0 -0
  52. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/05-active-storage-images/PLAN-01-SUMMARY.md +0 -0
  53. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/05-active-storage-images/PLAN-01.md +0 -0
  54. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/05-active-storage-images/PLAN-02-SUMMARY.md +0 -0
  55. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/05-active-storage-images/PLAN-02.md +0 -0
  56. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/06-netflix-feed-fix/06-VERIFICATION.md +0 -0
  57. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/06-netflix-feed-fix/PLAN-01-SUMMARY.md +0 -0
  58. /data/.vbw-planning/{phases → milestones/generator-enhancements/phases}/06-netflix-feed-fix/PLAN-01.md +0 -0
data/CHANGELOG.md CHANGED
@@ -15,6 +15,31 @@ All notable changes to this project are documented below. The format follows [Ke
15
15
 
16
16
  - No unreleased changes yet.
17
17
 
18
+ ## [0.5.1] - 2026-02-13
19
+
20
+ ### Changed
21
+
22
+ - Bumped puma from 7.1.0 to 7.2.0 (17% faster HTTP parsing, `workers :auto`, GC-compactible C extension).
23
+ - Bumped solid_queue from 1.2.4 to 1.3.1 (async mode, bug fixes).
24
+ - Bumped turbo-rails from 2.0.20 to 2.0.23 (broadcast suppression fix, navigator clobbering fix).
25
+
26
+ ## [0.5.0] - 2026-02-13
27
+
28
+ ### Added
29
+
30
+ - `bin/source_monitor upgrade` command: detects version changes since last install, copies new migrations, re-runs the generator, runs verification, and reports what changed. Uses a `.source_monitor_version` marker file for version tracking.
31
+ - `PendingMigrationsVerifier` checks for unmigrated SourceMonitor tables in the verification suite, integrated into both `bin/source_monitor verify` and the upgrade flow.
32
+ - Configuration deprecation framework: engine developers can register deprecated config options with `DeprecationRegistry.register`. At boot time, stale options trigger `:warning` (renamed) or `:error` (removed) messages with actionable replacement paths.
33
+ - `sm-upgrade` AI skill guides agents through post-update workflows: CHANGELOG parsing, running the upgrade command, interpreting verification results, and handling deprecation warnings.
34
+ - `docs/upgrade.md` versioned upgrade guide with general steps, version-specific notes (0.1.x through 0.4.x), and troubleshooting.
35
+ - `sm-host-setup` skill cross-references the upgrade workflow.
36
+
37
+ ### Testing
38
+
39
+ - 1,003 tests, 0 failures (up from 973 in 0.4.0).
40
+ - RuboCop: 397 files, 0 offenses.
41
+ - Brakeman: 0 warnings.
42
+
18
43
  ## [0.4.0] - 2026-02-12
19
44
 
20
45
  ### Added
data/CLAUDE.md CHANGED
@@ -4,9 +4,10 @@
4
4
 
5
5
  ## Active Context
6
6
 
7
- **Milestone:** none (archived)
8
- **Last shipped:** default (2026-02-10) -- 4 phases, 14 plans, 841 tests
9
- **Next action:** /vbw:plan to start new milestone
7
+ **Milestone:** (none active)
8
+ **Last shipped:** upgrade-assurance (2026-02-13) -- 3 phases, 14 tasks, 12 commits
9
+ **Previous:** generator-enhancements (2026-02-12) -- v0.4.0
10
+ **Next action:** /vbw:vibe to start a new milestone
10
11
 
11
12
  ## Key Decisions
12
13
 
@@ -192,6 +193,7 @@ Engine-specific skills (`sm-*` prefix). Consumer skills install by default; cont
192
193
  | `sm-event-handler` | Lifecycle callbacks (after_item_created, etc.) |
193
194
  | `sm-model-extension` | Extend engine models from host app |
194
195
  | `sm-dashboard-widget` | Dashboard queries, presenters, Turbo broadcasts |
196
+ | `sm-upgrade` | Gem upgrade workflow with CHANGELOG parsing |
195
197
 
196
198
  ### Contributor Skills (opt-in)
197
199
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- source_monitor (0.4.0)
4
+ source_monitor (0.5.1)
5
5
  cssbundling-rails (~> 1.4)
6
6
  faraday (~> 2.9)
7
7
  faraday-follow_redirects (~> 0.4)
@@ -241,7 +241,7 @@ GEM
241
241
  date
242
242
  stringio
243
243
  public_suffix (6.0.2)
244
- puma (7.1.0)
244
+ puma (7.2.0)
245
245
  nio4r (~> 2.0)
246
246
  raabro (1.4.0)
247
247
  racc (1.8.1)
@@ -349,7 +349,7 @@ GEM
349
349
  activejob (>= 7.2)
350
350
  activerecord (>= 7.2)
351
351
  railties (>= 7.2)
352
- solid_queue (1.2.4)
352
+ solid_queue (1.3.1)
353
353
  activejob (>= 7.1)
354
354
  activerecord (>= 7.1)
355
355
  concurrent-ruby (>= 1.3.1)
@@ -362,7 +362,7 @@ GEM
362
362
  thor (1.5.0)
363
363
  timeout (0.6.0)
364
364
  tsort (0.2.0)
365
- turbo-rails (2.0.20)
365
+ turbo-rails (2.0.23)
366
366
  actionpack (>= 7.1.0)
367
367
  railties (>= 7.1.0)
368
368
  tzinfo (2.0.6)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.5.1
data/docs/upgrade.md ADDED
@@ -0,0 +1,140 @@
1
+ # SourceMonitor Upgrade Guide
2
+
3
+ This guide covers upgrading SourceMonitor to a new gem version in your host Rails application.
4
+
5
+ ## General Upgrade Steps
6
+
7
+ 1. Review the [CHANGELOG](../CHANGELOG.md) for changes between your current and target versions
8
+ 2. Update your Gemfile version constraint and run `bundle update source_monitor`
9
+ 3. Run the upgrade command: `bin/source_monitor upgrade`
10
+ 4. Apply database migrations if new ones were copied: `bin/rails db:migrate`
11
+ 5. Address any deprecation warnings in your initializer (see Deprecation Handling below)
12
+ 6. Run verification: `bin/source_monitor verify`
13
+ 7. Restart your web server and background workers
14
+
15
+ ## Quick Upgrade (Most Cases)
16
+
17
+ ```bash
18
+ # 1. Update the gem
19
+ bundle update source_monitor
20
+
21
+ # 2. Run the upgrade command (handles migrations, generator, verification)
22
+ bin/source_monitor upgrade
23
+
24
+ # 3. Migrate if needed
25
+ bin/rails db:migrate
26
+
27
+ # 4. Restart
28
+ # (restart web server and Solid Queue workers)
29
+ ```
30
+
31
+ ## Deprecation Handling
32
+
33
+ When upgrading, you may see deprecation warnings in your Rails log:
34
+
35
+ ```
36
+ [SourceMonitor] DEPRECATION: 'http.old_option' was deprecated in v0.5.0 and replaced by 'http.new_option'.
37
+ ```
38
+
39
+ To resolve:
40
+ 1. Open `config/initializers/source_monitor.rb`
41
+ 2. Find the deprecated option (e.g., `config.http.old_option = value`)
42
+ 3. Replace with the new option from the warning message (e.g., `config.http.new_option = value`)
43
+ 4. Restart and verify the warning is gone
44
+
45
+ If a removed option raises an error (`SourceMonitor::DeprecatedOptionError`), you must update the initializer before the app can boot.
46
+
47
+ ## Version-Specific Notes
48
+
49
+ ### Upgrading to 0.4.0 (from 0.3.x)
50
+
51
+ **Released:** 2026-02-12
52
+
53
+ **What changed:**
54
+ - Install generator now auto-patches `Procfile.dev` with a Solid Queue `jobs:` entry
55
+ - Install generator now patches `config/queue.yml` dispatcher with `recurring_schedule: config/recurring.yml`
56
+ - Active Storage image download feature added (opt-in)
57
+ - SSL certificate configuration added to HTTP settings
58
+ - Enhanced verification messages for SolidQueue and RecurringSchedule verifiers
59
+
60
+ **Upgrade steps:**
61
+ ```bash
62
+ bundle update source_monitor
63
+ bin/source_monitor upgrade
64
+ bin/rails db:migrate
65
+ ```
66
+
67
+ **Notes:**
68
+ - No breaking changes. All existing configuration remains valid.
69
+ - Re-running the generator (`bin/rails generate source_monitor:install`) will add missing `Procfile.dev` and `queue.yml` entries without overwriting existing config.
70
+ - New optional features: `config.images.download_to_active_storage = true`, `config.http.ssl_ca_file`, `config.http.ssl_ca_path`, `config.http.ssl_verify`.
71
+
72
+ ### Upgrading to 0.3.0 (from 0.2.x)
73
+
74
+ **Released:** 2026-02-10
75
+
76
+ **What changed:**
77
+ - Internal refactoring: FeedFetcher, Configuration, ImportSessionsController, and ItemCreator extracted into smaller modules
78
+ - Eager requires replaced with Ruby autoload
79
+ - Skills system added (14 `sm-*` Claude Code skills)
80
+
81
+ **Upgrade steps:**
82
+ ```bash
83
+ bundle update source_monitor
84
+ bin/source_monitor upgrade
85
+ bin/rails db:migrate
86
+ ```
87
+
88
+ **Notes:**
89
+ - No breaking changes to the public API.
90
+ - If you referenced internal classes directly (e.g., `SourceMonitor::FeedFetcher` internals), verify your code against the new module structure.
91
+ - Optionally install AI skills: `bin/rails source_monitor:skills:install`
92
+
93
+ ### Upgrading to 0.2.0 (from 0.1.x)
94
+
95
+ **Released:** 2025-11-25
96
+
97
+ **What changed:**
98
+ - OPML import wizard with multi-step flow
99
+ - New `ImportHistory` model and associated migrations
100
+
101
+ **Upgrade steps:**
102
+ ```bash
103
+ bundle update source_monitor
104
+ bin/rails railties:install:migrations FROM=source_monitor
105
+ bin/rails db:migrate
106
+ ```
107
+
108
+ **Notes:**
109
+ - New database tables required. Run migrations after updating.
110
+ - No configuration changes needed.
111
+
112
+ ## Troubleshooting
113
+
114
+ ### "Already up to date" but I expected changes
115
+ - Verify the gem version actually changed: `bundle show source_monitor`
116
+ - Check `Gemfile.lock` for the resolved version
117
+ - If the `.source_monitor_version` marker was manually edited, delete it and re-run upgrade
118
+
119
+ ### Migrations fail with duplicate timestamps
120
+ - Remove the duplicate migration file from `db/migrate/` (keep the newer one)
121
+ - Re-run `bin/rails db:migrate`
122
+
123
+ ### Deprecation error prevents boot
124
+ - Read the error message for the replacement option
125
+ - Update your initializer before restarting
126
+ - If unsure which option to use, consult [Configuration Reference](configuration.md)
127
+
128
+ ### Verification failures after upgrade
129
+ - **PendingMigrations:** Run `bin/rails db:migrate`
130
+ - **SolidQueue:** Ensure workers are running. Check `Procfile.dev` for a `jobs:` entry.
131
+ - **RecurringSchedule:** Re-run `bin/rails generate source_monitor:install` to patch `config/queue.yml`
132
+ - **ActionCable:** Configure Solid Cable or Redis adapter
133
+
134
+ For additional help, see [Troubleshooting](troubleshooting.md).
135
+
136
+ ## See Also
137
+ - [Setup Guide](setup.md) -- Initial installation
138
+ - [Configuration Reference](configuration.md) -- All configuration options
139
+ - [Troubleshooting](troubleshooting.md) -- Common issues and fixes
140
+ - [CHANGELOG](../CHANGELOG.md) -- Full version history
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ class DeprecatedOptionError < StandardError; end
5
+
6
+ class Configuration
7
+ # Registry for deprecated configuration options.
8
+ #
9
+ # Engine developers register deprecations at boot time via the DSL:
10
+ #
11
+ # SourceMonitor::Configuration::DeprecationRegistry.register(
12
+ # "http.old_proxy_url",
13
+ # removed_in: "0.5.0",
14
+ # replacement: "http.proxy",
15
+ # severity: :warning,
16
+ # message: "Use config.http.proxy instead"
17
+ # )
18
+ #
19
+ # When a host app's initializer accesses a deprecated option, the
20
+ # trapping method fires automatically:
21
+ # - :warning -- logs via Rails.logger.warn and forwards to replacement
22
+ # - :error -- raises SourceMonitor::DeprecatedOptionError
23
+ #
24
+ class DeprecationRegistry
25
+ # Maps settings accessor names (as used on Configuration) to their classes.
26
+ SETTINGS_CLASSES = {
27
+ "http" => "HTTPSettings",
28
+ "fetching" => "FetchingSettings",
29
+ "health" => "HealthSettings",
30
+ "scraping" => "ScrapingSettings",
31
+ "retention" => "RetentionSettings",
32
+ "realtime" => "RealtimeSettings",
33
+ "authentication" => "AuthenticationSettings",
34
+ "images" => "ImagesSettings",
35
+ "scrapers" => "ScraperRegistry",
36
+ "events" => "Events",
37
+ "models" => "Models"
38
+ }.freeze
39
+
40
+ class << self
41
+ # Register a deprecated configuration option.
42
+ #
43
+ # @param path [String] dot-notation path, e.g. "http.old_proxy_url" or "old_queue_prefix"
44
+ # @param removed_in [String] version in which the option was deprecated
45
+ # @param replacement [String, nil] dot-notation path to the replacement option
46
+ # @param severity [:warning, :error] :warning logs + forwards, :error raises
47
+ # @param message [String, nil] additional migration guidance
48
+ def register(path, removed_in:, replacement: nil, severity: :warning, message: nil)
49
+ segments = path.split(".")
50
+ source_prefix = nil
51
+ if segments.length == 1
52
+ target_class = Configuration
53
+ option_name = segments.first
54
+ else
55
+ source_prefix = segments.first
56
+ option_name = segments.last
57
+ class_name = SETTINGS_CLASSES[source_prefix]
58
+ raise ArgumentError, "Unknown settings accessor: #{source_prefix}" unless class_name
59
+
60
+ target_class = Configuration.const_get(class_name)
61
+ end
62
+
63
+ deprecation_message = build_message(path, removed_in, replacement, message)
64
+
65
+ if target_class.method_defined?(:"#{option_name}=") || target_class.method_defined?(option_name.to_sym)
66
+ warn "[SourceMonitor] DeprecationRegistry: '#{path}' already exists on #{target_class.name}. " \
67
+ "Skipping trap definition -- the option is not yet removed/renamed."
68
+ entries[path] = { path: path, removed_in: removed_in, replacement: replacement,
69
+ severity: severity, message: deprecation_message, skipped: true }
70
+ return
71
+ end
72
+
73
+ define_trap_methods(target_class, option_name, deprecation_message, severity, replacement,
74
+ source_prefix: source_prefix)
75
+
76
+ entries[path] = { path: path, removed_in: removed_in, replacement: replacement,
77
+ severity: severity, message: deprecation_message, skipped: false }
78
+ end
79
+
80
+ # Remove all registered deprecation traps and clear state.
81
+ # Essential for test isolation.
82
+ def clear!
83
+ defined_methods.each do |target_class, method_name|
84
+ target_class.remove_method(method_name) if target_class.method_defined?(method_name)
85
+ rescue NameError
86
+ # Method was already removed or never defined; ignore.
87
+ end
88
+
89
+ @entries = {}
90
+ @defined_methods = []
91
+ end
92
+
93
+ # Returns a duplicate of the entries hash for inspection.
94
+ def entries
95
+ @entries ||= {}
96
+ end
97
+
98
+ # Check if a path is registered.
99
+ def registered?(path)
100
+ entries.key?(path)
101
+ end
102
+
103
+ # No-op hook for future "default changed" checks.
104
+ # Called by Configuration#check_deprecations! after the configure block.
105
+ def check_defaults!(_config)
106
+ # Reserved for future use. Phases may add checks like:
107
+ # "option X changed its default from A to B in version Y"
108
+ end
109
+
110
+ private
111
+
112
+ def defined_methods
113
+ @defined_methods ||= []
114
+ end
115
+
116
+ def build_message(path, removed_in, replacement, extra_message)
117
+ parts = +"[SourceMonitor] DEPRECATION: '#{path}' was deprecated in v#{removed_in}"
118
+ parts << " and replaced by '#{replacement}'" if replacement
119
+ parts << ". #{extra_message}" if extra_message
120
+ parts << "." unless parts.end_with?(".")
121
+ parts.freeze
122
+ end
123
+
124
+ def define_trap_methods(target_class, option_name, deprecation_message, severity, replacement, source_prefix: nil)
125
+ writer_name = :"#{option_name}="
126
+ reader_name = option_name.to_sym
127
+
128
+ case severity
129
+ when :warning
130
+ define_warning_writer(target_class, writer_name, deprecation_message, replacement, source_prefix)
131
+ define_warning_reader(target_class, reader_name, deprecation_message, replacement, source_prefix)
132
+ when :error
133
+ define_error_method(target_class, writer_name, deprecation_message)
134
+ define_error_method(target_class, reader_name, deprecation_message)
135
+ else
136
+ raise ArgumentError, "Unknown severity: #{severity}. Must be :warning or :error."
137
+ end
138
+
139
+ defined_methods.push([ target_class, writer_name ], [ target_class, reader_name ])
140
+ end
141
+
142
+ def define_warning_writer(target_class, writer_name, deprecation_message, replacement, source_prefix)
143
+ replacement_writer = replacement_setter_for(replacement, source_prefix)
144
+
145
+ target_class.define_method(writer_name) do |value|
146
+ Rails.logger.warn(deprecation_message)
147
+ if replacement_writer
148
+ resolve_replacement_target(replacement_writer[:target]).public_send(
149
+ replacement_writer[:setter], value
150
+ )
151
+ end
152
+ end
153
+ end
154
+
155
+ def define_warning_reader(target_class, reader_name, deprecation_message, replacement, source_prefix)
156
+ replacement_reader = replacement_getter_for(replacement, source_prefix)
157
+
158
+ target_class.define_method(reader_name) do
159
+ Rails.logger.warn(deprecation_message)
160
+ if replacement_reader
161
+ resolve_replacement_target(replacement_reader[:target]).public_send(
162
+ replacement_reader[:getter]
163
+ )
164
+ end
165
+ end
166
+ end
167
+
168
+ def define_error_method(target_class, method_name, deprecation_message)
169
+ target_class.define_method(method_name) do |*|
170
+ raise SourceMonitor::DeprecatedOptionError, deprecation_message
171
+ end
172
+ end
173
+
174
+ # Parse replacement path into target accessor chain and setter name.
175
+ # When source_prefix matches the replacement prefix, the target is nil
176
+ # (replacement is on the same settings class).
177
+ #
178
+ # "http.proxy" with source_prefix "http" => { target: nil, setter: "proxy=" }
179
+ # "queue_namespace" => { target: nil, setter: "queue_namespace=" }
180
+ # "http.proxy" with source_prefix nil => { target: :http, setter: "proxy=" }
181
+ def replacement_setter_for(replacement, source_prefix = nil)
182
+ return nil unless replacement
183
+
184
+ segments = replacement.split(".")
185
+ if segments.length == 1
186
+ { target: nil, setter: :"#{segments.first}=" }
187
+ elsif source_prefix && segments.first == source_prefix
188
+ { target: nil, setter: :"#{segments.last}=" }
189
+ else
190
+ { target: segments.first.to_sym, setter: :"#{segments.last}=" }
191
+ end
192
+ end
193
+
194
+ # Parse replacement path into target accessor chain and getter name.
195
+ def replacement_getter_for(replacement, source_prefix = nil)
196
+ return nil unless replacement
197
+
198
+ segments = replacement.split(".")
199
+ if segments.length == 1
200
+ { target: nil, getter: segments.first.to_sym }
201
+ elsif source_prefix && segments.first == source_prefix
202
+ { target: nil, getter: segments.last.to_sym }
203
+ else
204
+ { target: segments.first.to_sym, getter: segments.last.to_sym }
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ # Add a helper method to settings classes and Configuration for resolving
213
+ # replacement targets. This allows "http.proxy" to resolve as self.http.proxy
214
+ # from within a Configuration instance, or as self.proxy from within an
215
+ # HTTPSettings instance.
216
+ module SourceMonitor
217
+ class Configuration
218
+ private
219
+
220
+ def resolve_replacement_target(accessor)
221
+ accessor ? public_send(accessor) : self
222
+ end
223
+ end
224
+ end
225
+
226
+ # Add the same helper to all settings classes so forwarding works
227
+ # when the deprecated method is defined on a nested settings class
228
+ # and the replacement is on the same class (e.g. "http.old_proxy" -> "http.proxy").
229
+ SourceMonitor::Configuration::DeprecationRegistry::SETTINGS_CLASSES.each_value do |class_name|
230
+ klass = SourceMonitor::Configuration.const_get(class_name)
231
+ unless klass.method_defined?(:resolve_replacement_target, false)
232
+ klass.define_method(:resolve_replacement_target) do |accessor|
233
+ accessor ? public_send(accessor) : self
234
+ end
235
+ klass.send(:private, :resolve_replacement_target)
236
+ end
237
+ end
@@ -14,6 +14,7 @@ require "source_monitor/configuration/events"
14
14
  require "source_monitor/configuration/validation_definition"
15
15
  require "source_monitor/configuration/model_definition"
16
16
  require "source_monitor/configuration/models"
17
+ require "source_monitor/configuration/deprecation_registry"
17
18
 
18
19
  module SourceMonitor
19
20
  class Configuration
@@ -85,5 +86,12 @@ module SourceMonitor
85
86
  raise ArgumentError, "unknown queue role #{role.inspect}"
86
87
  end
87
88
  end
89
+
90
+ # Post-configure hook for deprecation validation.
91
+ # Delegates to DeprecationRegistry.check_defaults! for future
92
+ # "default changed" checks. Currently a no-op.
93
+ def check_deprecations!
94
+ DeprecationRegistry.check_defaults!(self)
95
+ end
88
96
  end
89
97
  end
@@ -23,6 +23,13 @@ module SourceMonitor
23
23
  handle_summary(summary)
24
24
  end
25
25
 
26
+ desc "upgrade", "Upgrade SourceMonitor after a gem version change"
27
+ def upgrade
28
+ command = UpgradeCommand.new
29
+ summary = command.call
30
+ handle_summary(summary)
31
+ end
32
+
26
33
  private
27
34
 
28
35
  def handle_summary(summary)
@@ -11,6 +11,7 @@ module SourceMonitor
11
11
  CONSUMER_SKILLS = %w[
12
12
  sm-host-setup sm-configure sm-scraper-adapter
13
13
  sm-event-handler sm-model-extension sm-dashboard-widget
14
+ sm-upgrade
14
15
  ].freeze
15
16
 
16
17
  CONTRIBUTOR_SKILLS = %w[
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ module Setup
5
+ class UpgradeCommand
6
+ def initialize(
7
+ migration_installer: MigrationInstaller.new,
8
+ install_generator: InstallGenerator.new,
9
+ verifier: Verification::Runner.new,
10
+ version_file: File.join(Dir.pwd, ".source_monitor_version"),
11
+ current_version: SourceMonitor::VERSION
12
+ )
13
+ @migration_installer = migration_installer
14
+ @install_generator = install_generator
15
+ @verifier = verifier
16
+ @version_file = version_file
17
+ @current_version = current_version
18
+ end
19
+
20
+ def call
21
+ stored = read_stored_version
22
+
23
+ if stored == current_version
24
+ return up_to_date_summary
25
+ end
26
+
27
+ migration_installer.install
28
+ install_generator.run
29
+ summary = verifier.call
30
+ write_version_marker
31
+ summary
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :migration_installer, :install_generator, :verifier, :version_file, :current_version
37
+
38
+ def read_stored_version
39
+ return nil unless File.exist?(version_file)
40
+
41
+ File.read(version_file).strip
42
+ end
43
+
44
+ def write_version_marker
45
+ File.write(version_file, current_version)
46
+ end
47
+
48
+ def up_to_date_summary
49
+ result = Verification::Result.new(
50
+ key: :upgrade,
51
+ name: "Upgrade",
52
+ status: :ok,
53
+ details: "Already up to date (v#{current_version})"
54
+ )
55
+ Verification::Summary.new([ result ])
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceMonitor
4
+ module Setup
5
+ module Verification
6
+ class PendingMigrationsVerifier
7
+ MIGRATION_TIMESTAMP_PATTERN = /\A\d+_/
8
+
9
+ def initialize(
10
+ engine_migrations_path: default_engine_migrations_path,
11
+ host_migrations_path: default_host_migrations_path,
12
+ connection: default_connection
13
+ )
14
+ @engine_migrations_path = engine_migrations_path
15
+ @host_migrations_path = host_migrations_path
16
+ @connection = connection
17
+ end
18
+
19
+ def call
20
+ engine_names = source_monitor_migration_names(engine_migrations_path)
21
+ return ok_result("No SourceMonitor engine migrations found") if engine_names.empty?
22
+
23
+ host_names = migration_names(host_migrations_path)
24
+ missing = engine_names - host_names
25
+
26
+ if missing.any?
27
+ warning_result(
28
+ "#{missing.size} SourceMonitor migration(s) not copied to host: #{missing.join(', ')}",
29
+ "Run `bin/source_monitor upgrade` or `bin/rails railties:install:migrations FROM=source_monitor`"
30
+ )
31
+ elsif connection.migration_context.needs_migration?
32
+ warning_result(
33
+ "All SourceMonitor migrations are copied but some migrations are pending",
34
+ "Run `bin/rails db:migrate` to apply pending migrations"
35
+ )
36
+ else
37
+ ok_result("All SourceMonitor migrations are present and up to date")
38
+ end
39
+ rescue StandardError => e
40
+ error_result(
41
+ "Migration verification failed: #{e.message}",
42
+ "Check database connectivity and migration file permissions"
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :engine_migrations_path, :host_migrations_path, :connection
49
+
50
+ def default_engine_migrations_path
51
+ SourceMonitor::Engine.root.join("db/migrate")
52
+ end
53
+
54
+ def default_host_migrations_path
55
+ Rails.root.join("db/migrate")
56
+ end
57
+
58
+ def default_connection
59
+ ActiveRecord::Base.connection
60
+ end
61
+
62
+ def source_monitor_migration_names(path)
63
+ migration_names(path).select { |name| name.include?("source_monitor") }
64
+ end
65
+
66
+ def migration_names(path)
67
+ return [] unless File.directory?(path.to_s)
68
+
69
+ Dir.children(path.to_s)
70
+ .select { |f| f.end_with?(".rb") }
71
+ .map { |f| strip_timestamp(f) }
72
+ end
73
+
74
+ def strip_timestamp(filename)
75
+ filename.sub(MIGRATION_TIMESTAMP_PATTERN, "").delete_suffix(".rb")
76
+ end
77
+
78
+ def ok_result(details)
79
+ Result.new(key: :pending_migrations, name: "Pending Migrations", status: :ok, details: details)
80
+ end
81
+
82
+ def warning_result(details, remediation)
83
+ Result.new(key: :pending_migrations, name: "Pending Migrations", status: :warning, details: details, remediation: remediation)
84
+ end
85
+
86
+ def error_result(details, remediation)
87
+ Result.new(key: :pending_migrations, name: "Pending Migrations", status: :error, details: details, remediation: remediation)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -18,7 +18,7 @@ module SourceMonitor
18
18
  attr_reader :verifiers
19
19
 
20
20
  def default_verifiers
21
- [ SolidQueueVerifier.new, RecurringScheduleVerifier.new, ActionCableVerifier.new ]
21
+ [ PendingMigrationsVerifier.new, SolidQueueVerifier.new, RecurringScheduleVerifier.new, ActionCableVerifier.new ]
22
22
  end
23
23
  end
24
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SourceMonitor
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.1"
5
5
  end