source_monitor 0.3.3 → 0.5.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/.claude/commands/release.md +101 -58
- data/.claude/skills/sm-configure/SKILL.md +13 -2
- data/.claude/skills/sm-configure/reference/configuration-reference.md +33 -0
- data/.claude/skills/sm-host-setup/SKILL.md +18 -2
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +33 -0
- data/.claude/skills/sm-job/SKILL.md +1 -1
- data/.claude/skills/sm-upgrade/SKILL.md +102 -0
- data/.claude/skills/sm-upgrade/reference/upgrade-workflow.md +92 -0
- data/.claude/skills/sm-upgrade/reference/version-history.md +68 -0
- data/.vbw-planning/SHIPPED.md +35 -0
- data/.vbw-planning/config.json +24 -1
- data/.vbw-planning/discovery.json +3 -1
- data/.vbw-planning/{REQUIREMENTS.md → milestones/generator-enhancements/REQUIREMENTS.md} +22 -0
- data/.vbw-planning/milestones/generator-enhancements/ROADMAP.md +125 -0
- data/.vbw-planning/milestones/generator-enhancements/SHIPPED.md +40 -0
- data/.vbw-planning/milestones/generator-enhancements/STATE.md +43 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-VERIFICATION.md +86 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01.md +380 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/02-VERIFICATION.md +78 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01-SUMMARY.md +46 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01.md +500 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/03-VERIFICATION.md +89 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01-SUMMARY.md +48 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01.md +456 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/04-VERIFICATION.md +129 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01-SUMMARY.md +70 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01.md +747 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/05-VERIFICATION.md +156 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01-SUMMARY.md +69 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01.md +455 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02-SUMMARY.md +39 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02.md +488 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/06-VERIFICATION.md +100 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md +37 -0
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01.md +345 -0
- data/.vbw-planning/milestones/upgrade-assurance/REQUIREMENTS.md +80 -0
- data/.vbw-planning/milestones/upgrade-assurance/ROADMAP.md +75 -0
- data/.vbw-planning/milestones/upgrade-assurance/STATE.md +29 -0
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/01-VERIFICATION.md +144 -0
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01-SUMMARY.md +43 -0
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01.md +405 -0
- data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01-SUMMARY.md +27 -0
- data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01.md +303 -0
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/03-VERIFICATION.md +380 -0
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01-SUMMARY.md +36 -0
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01.md +652 -0
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +5 -3
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/app/assets/builds/source_monitor/application.css +9 -0
- data/app/helpers/source_monitor/application_helper.rb +38 -0
- data/app/jobs/source_monitor/download_content_images_job.rb +72 -0
- data/app/models/source_monitor/item_content.rb +2 -0
- data/app/views/source_monitor/dashboard/_recent_activity.html.erb +9 -0
- data/app/views/source_monitor/items/_details.html.erb +2 -2
- data/app/views/source_monitor/logs/index.html.erb +9 -0
- data/app/views/source_monitor/sources/_details.html.erb +2 -2
- data/app/views/source_monitor/sources/_row.html.erb +1 -1
- data/docs/setup.md +10 -1
- data/docs/troubleshooting.md +38 -7
- data/docs/upgrade.md +140 -0
- data/lib/generators/source_monitor/install/install_generator.rb +101 -0
- data/lib/source_monitor/configuration/deprecation_registry.rb +237 -0
- data/lib/source_monitor/configuration/http_settings.rb +7 -1
- data/lib/source_monitor/configuration/images_settings.rb +37 -0
- data/lib/source_monitor/configuration.rb +11 -1
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +16 -7
- data/lib/source_monitor/dashboard/recent_activity.rb +1 -0
- data/lib/source_monitor/dashboard/recent_activity_presenter.rb +15 -2
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +13 -0
- data/lib/source_monitor/http.rb +23 -0
- data/lib/source_monitor/images/content_rewriter.rb +81 -0
- data/lib/source_monitor/images/downloader.rb +82 -0
- data/lib/source_monitor/logs/table_presenter.rb +25 -0
- data/lib/source_monitor/setup/cli.rb +7 -0
- data/lib/source_monitor/setup/procfile_patcher.rb +31 -0
- data/lib/source_monitor/setup/queue_config_patcher.rb +84 -0
- data/lib/source_monitor/setup/skills_installer.rb +1 -0
- data/lib/source_monitor/setup/upgrade_command.rb +59 -0
- data/lib/source_monitor/setup/verification/pending_migrations_verifier.rb +92 -0
- data/lib/source_monitor/setup/verification/recurring_schedule_verifier.rb +102 -0
- data/lib/source_monitor/setup/verification/runner.rb +1 -1
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +1 -1
- data/lib/source_monitor/setup/workflow.rb +10 -0
- data/lib/source_monitor/version.rb +1 -1
- data/lib/source_monitor.rb +11 -0
- metadata +51 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokolexbor"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module SourceMonitor
|
|
7
|
+
module Images
|
|
8
|
+
class ContentRewriter
|
|
9
|
+
attr_reader :html, :base_url
|
|
10
|
+
|
|
11
|
+
def initialize(html, base_url: nil)
|
|
12
|
+
@html = html.to_s
|
|
13
|
+
@base_url = base_url
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns an array of absolute image URLs found in <img> tags.
|
|
17
|
+
# Skips data: URIs, blank src, and invalid URLs.
|
|
18
|
+
def image_urls
|
|
19
|
+
return [] if html.blank?
|
|
20
|
+
|
|
21
|
+
doc = parse_fragment
|
|
22
|
+
urls = []
|
|
23
|
+
|
|
24
|
+
doc.css("img[src]").each do |img|
|
|
25
|
+
url = resolve_url(img["src"])
|
|
26
|
+
urls << url if url && downloadable_url?(url)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
urls.uniq
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Rewrites <img src="..."> attributes by yielding each original URL
|
|
33
|
+
# to the block and replacing with the block's return value.
|
|
34
|
+
# Returns the rewritten HTML string.
|
|
35
|
+
# If the block returns nil, the original URL is preserved (graceful fallback).
|
|
36
|
+
def rewrite
|
|
37
|
+
return html if html.blank?
|
|
38
|
+
|
|
39
|
+
doc = parse_fragment
|
|
40
|
+
|
|
41
|
+
doc.css("img[src]").each do |img|
|
|
42
|
+
original_url = resolve_url(img["src"])
|
|
43
|
+
next unless original_url && downloadable_url?(original_url)
|
|
44
|
+
|
|
45
|
+
new_url = yield(original_url)
|
|
46
|
+
img["src"] = new_url if new_url.present?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
doc.to_html
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def parse_fragment
|
|
55
|
+
Nokolexbor::DocumentFragment.parse(html)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve_url(src)
|
|
59
|
+
src = src.to_s.strip
|
|
60
|
+
return nil if src.blank?
|
|
61
|
+
return nil if src.start_with?("data:")
|
|
62
|
+
|
|
63
|
+
uri = URI.parse(src)
|
|
64
|
+
if uri.relative? && base_url.present?
|
|
65
|
+
URI.join(base_url, src).to_s
|
|
66
|
+
elsif uri.absolute?
|
|
67
|
+
src
|
|
68
|
+
end
|
|
69
|
+
rescue URI::InvalidURIError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def downloadable_url?(url)
|
|
74
|
+
uri = URI.parse(url)
|
|
75
|
+
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
76
|
+
rescue URI::InvalidURIError
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module SourceMonitor
|
|
7
|
+
module Images
|
|
8
|
+
class Downloader
|
|
9
|
+
Result = Struct.new(:io, :filename, :content_type, :byte_size, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
attr_reader :url, :settings
|
|
12
|
+
|
|
13
|
+
def initialize(url, settings: nil)
|
|
14
|
+
@url = url
|
|
15
|
+
@settings = settings || SourceMonitor.config.images
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Downloads the image and returns a Result, or nil if download fails
|
|
19
|
+
# or the image does not meet validation criteria.
|
|
20
|
+
def call
|
|
21
|
+
response = fetch_image
|
|
22
|
+
return unless response
|
|
23
|
+
|
|
24
|
+
content_type = response.headers["content-type"]&.split(";")&.first&.strip&.downcase
|
|
25
|
+
return unless allowed_content_type?(content_type)
|
|
26
|
+
|
|
27
|
+
body = response.body
|
|
28
|
+
return unless body && body.bytesize > 0
|
|
29
|
+
return if body.bytesize > settings.max_download_size
|
|
30
|
+
|
|
31
|
+
filename = derive_filename(url, content_type)
|
|
32
|
+
|
|
33
|
+
Result.new(
|
|
34
|
+
io: StringIO.new(body),
|
|
35
|
+
filename: filename,
|
|
36
|
+
content_type: content_type,
|
|
37
|
+
byte_size: body.bytesize
|
|
38
|
+
)
|
|
39
|
+
rescue Faraday::Error, URI::InvalidURIError, Timeout::Error
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def fetch_image
|
|
46
|
+
connection = Faraday.new do |f|
|
|
47
|
+
f.options.timeout = settings.download_timeout
|
|
48
|
+
f.options.open_timeout = [ settings.download_timeout / 2, 5 ].min
|
|
49
|
+
f.headers["User-Agent"] = SourceMonitor.config.http.user_agent || "SourceMonitor/#{SourceMonitor::VERSION}"
|
|
50
|
+
f.headers["Accept"] = "image/*"
|
|
51
|
+
f.adapter Faraday.default_adapter
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
response = connection.get(url)
|
|
55
|
+
return response if response.status == 200
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def allowed_content_type?(content_type)
|
|
61
|
+
return false if content_type.blank?
|
|
62
|
+
|
|
63
|
+
settings.allowed_content_types.include?(content_type)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def derive_filename(image_url, content_type)
|
|
67
|
+
uri = URI.parse(image_url)
|
|
68
|
+
basename = File.basename(uri.path) if uri.path.present?
|
|
69
|
+
|
|
70
|
+
if basename.present? && basename.include?(".")
|
|
71
|
+
basename
|
|
72
|
+
else
|
|
73
|
+
ext = Rack::Mime::MIME_TYPES.invert[content_type] || ".bin"
|
|
74
|
+
"image-#{SecureRandom.hex(8)}#{ext}"
|
|
75
|
+
end
|
|
76
|
+
rescue URI::InvalidURIError
|
|
77
|
+
ext = Rack::Mime::MIME_TYPES.invert[content_type] || ".bin"
|
|
78
|
+
"image-#{SecureRandom.hex(8)}#{ext}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -61,6 +61,22 @@ module SourceMonitor
|
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
def url_label
|
|
65
|
+
if fetch?
|
|
66
|
+
domain_from_feed_url
|
|
67
|
+
elsif scrape?
|
|
68
|
+
entry.item&.url
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def url_href
|
|
73
|
+
if fetch?
|
|
74
|
+
entry.source&.feed_url
|
|
75
|
+
elsif scrape?
|
|
76
|
+
entry.item&.url
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
64
80
|
def source_label
|
|
65
81
|
entry.source&.name
|
|
66
82
|
end
|
|
@@ -142,6 +158,15 @@ module SourceMonitor
|
|
|
142
158
|
private
|
|
143
159
|
|
|
144
160
|
attr_reader :entry, :url_helpers
|
|
161
|
+
|
|
162
|
+
def domain_from_feed_url
|
|
163
|
+
feed_url = entry.source&.feed_url
|
|
164
|
+
return nil if feed_url.blank?
|
|
165
|
+
|
|
166
|
+
URI.parse(feed_url.to_s).host
|
|
167
|
+
rescue URI::InvalidURIError
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
145
170
|
end
|
|
146
171
|
|
|
147
172
|
def initialize(entries:, url_helpers:)
|
|
@@ -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)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module SourceMonitor
|
|
6
|
+
module Setup
|
|
7
|
+
class ProcfilePatcher
|
|
8
|
+
JOBS_ENTRY = "jobs: bundle exec rake solid_queue:start"
|
|
9
|
+
|
|
10
|
+
def initialize(path: "Procfile.dev")
|
|
11
|
+
@path = Pathname.new(path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def patch
|
|
15
|
+
if path.exist?
|
|
16
|
+
content = path.read
|
|
17
|
+
return false if content.match?(/^jobs:/)
|
|
18
|
+
|
|
19
|
+
path.open("a") { |f| f.puts("", JOBS_ENTRY) }
|
|
20
|
+
else
|
|
21
|
+
path.write("web: bin/rails server -p 3000\n#{JOBS_ENTRY}\n")
|
|
22
|
+
end
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :path
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module SourceMonitor
|
|
7
|
+
module Setup
|
|
8
|
+
class QueueConfigPatcher
|
|
9
|
+
RECURRING_SCHEDULE_VALUE = "config/recurring.yml"
|
|
10
|
+
|
|
11
|
+
DEFAULT_DISPATCHER = {
|
|
12
|
+
"polling_interval" => 1,
|
|
13
|
+
"batch_size" => 500,
|
|
14
|
+
"recurring_schedule" => RECURRING_SCHEDULE_VALUE
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(path: "config/queue.yml")
|
|
18
|
+
@path = Pathname.new(path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def patch
|
|
22
|
+
return false unless path.exist?
|
|
23
|
+
|
|
24
|
+
parsed = YAML.safe_load(path.read, aliases: true) || {}
|
|
25
|
+
return false if has_recurring_schedule?(parsed)
|
|
26
|
+
|
|
27
|
+
add_recurring_schedule!(parsed)
|
|
28
|
+
path.write(YAML.dump(parsed))
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :path
|
|
35
|
+
|
|
36
|
+
def has_recurring_schedule?(parsed)
|
|
37
|
+
parsed.each_value do |value|
|
|
38
|
+
next unless value.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
dispatchers = value["dispatchers"]
|
|
41
|
+
if dispatchers.is_a?(Array)
|
|
42
|
+
return true if dispatchers.any? { |d| d.is_a?(Hash) && d.key?("recurring_schedule") }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
return true if has_recurring_schedule?(value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if parsed.key?("dispatchers") && parsed["dispatchers"].is_a?(Array)
|
|
49
|
+
return true if parsed["dispatchers"].any? { |d| d.is_a?(Hash) && d.key?("recurring_schedule") }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def add_recurring_schedule!(parsed)
|
|
56
|
+
found_dispatchers = false
|
|
57
|
+
|
|
58
|
+
parsed.each_value do |value|
|
|
59
|
+
next unless value.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
if value.key?("dispatchers") && value["dispatchers"].is_a?(Array)
|
|
62
|
+
value["dispatchers"].each do |dispatcher|
|
|
63
|
+
next unless dispatcher.is_a?(Hash)
|
|
64
|
+
dispatcher["recurring_schedule"] ||= RECURRING_SCHEDULE_VALUE
|
|
65
|
+
end
|
|
66
|
+
found_dispatchers = true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if parsed.key?("dispatchers") && parsed["dispatchers"].is_a?(Array)
|
|
71
|
+
parsed["dispatchers"].each do |dispatcher|
|
|
72
|
+
next unless dispatcher.is_a?(Hash)
|
|
73
|
+
dispatcher["recurring_schedule"] ||= RECURRING_SCHEDULE_VALUE
|
|
74
|
+
end
|
|
75
|
+
found_dispatchers = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless found_dispatchers
|
|
79
|
+
parsed["dispatchers"] = [ DEFAULT_DISPATCHER.dup ]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SourceMonitor
|
|
4
|
+
module Setup
|
|
5
|
+
module Verification
|
|
6
|
+
class RecurringScheduleVerifier
|
|
7
|
+
SOURCE_MONITOR_KEY_PREFIX = "source_monitor_"
|
|
8
|
+
SOURCE_MONITOR_NAMESPACE = "SourceMonitor::"
|
|
9
|
+
|
|
10
|
+
def initialize(task_relation: default_task_relation, connection: default_connection)
|
|
11
|
+
@task_relation = task_relation
|
|
12
|
+
@connection = connection
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
return missing_gem_result unless task_relation
|
|
17
|
+
return missing_tables_result unless tables_present?
|
|
18
|
+
|
|
19
|
+
tasks = all_tasks
|
|
20
|
+
sm_tasks = source_monitor_tasks(tasks)
|
|
21
|
+
|
|
22
|
+
if sm_tasks.any?
|
|
23
|
+
ok_result("#{sm_tasks.size} SourceMonitor recurring task(s) registered")
|
|
24
|
+
elsif tasks.any?
|
|
25
|
+
warning_result(
|
|
26
|
+
"Recurring tasks exist but none belong to SourceMonitor",
|
|
27
|
+
"Add SourceMonitor entries to config/recurring.yml and ensure the dispatcher has `recurring_schedule: config/recurring.yml`"
|
|
28
|
+
)
|
|
29
|
+
else
|
|
30
|
+
warning_result(
|
|
31
|
+
"No recurring tasks are registered with Solid Queue",
|
|
32
|
+
"Configure a dispatcher with `recurring_schedule: config/recurring.yml` in config/queue.yml and ensure recurring.yml contains SourceMonitor task entries"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
error_result(
|
|
37
|
+
"Recurring schedule verification failed: #{e.message}",
|
|
38
|
+
"Verify Solid Queue migrations are up to date and the dispatcher is configured with recurring_schedule"
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :task_relation, :connection
|
|
45
|
+
|
|
46
|
+
def default_task_relation
|
|
47
|
+
SolidQueue::RecurringTask if defined?(SolidQueue::RecurringTask)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def default_connection
|
|
51
|
+
SolidQueue::RecurringTask.connection if defined?(SolidQueue::RecurringTask)
|
|
52
|
+
rescue StandardError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def tables_present?
|
|
57
|
+
return false unless connection
|
|
58
|
+
|
|
59
|
+
connection.table_exists?(task_relation.table_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def all_tasks
|
|
63
|
+
task_relation.all.to_a
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def source_monitor_tasks(tasks)
|
|
67
|
+
tasks.select do |task|
|
|
68
|
+
task.key.start_with?(SOURCE_MONITOR_KEY_PREFIX) ||
|
|
69
|
+
task.class_name.to_s.start_with?(SOURCE_MONITOR_NAMESPACE) ||
|
|
70
|
+
task.command.to_s.include?(SOURCE_MONITOR_NAMESPACE)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def missing_gem_result
|
|
75
|
+
error_result(
|
|
76
|
+
"Solid Queue gem is not available",
|
|
77
|
+
"Add `solid_queue` to your Gemfile and bundle install"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def missing_tables_result
|
|
82
|
+
error_result(
|
|
83
|
+
"Solid Queue recurring tasks table is missing",
|
|
84
|
+
"Run `rails solid_queue:install` or copy the engine's Solid Queue migration"
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ok_result(details)
|
|
89
|
+
Result.new(key: :recurring_schedule, name: "Recurring Schedule", status: :ok, details: details)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def warning_result(details, remediation)
|
|
93
|
+
Result.new(key: :recurring_schedule, name: "Recurring Schedule", status: :warning, details: details, remediation: remediation)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def error_result(details, remediation)
|
|
97
|
+
Result.new(key: :recurring_schedule, name: "Recurring Schedule", status: :error, details: details, remediation: remediation)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -18,7 +18,7 @@ module SourceMonitor
|
|
|
18
18
|
attr_reader :verifiers
|
|
19
19
|
|
|
20
20
|
def default_verifiers
|
|
21
|
-
[ SolidQueueVerifier.new, ActionCableVerifier.new ]
|
|
21
|
+
[ PendingMigrationsVerifier.new, SolidQueueVerifier.new, RecurringScheduleVerifier.new, ActionCableVerifier.new ]
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -21,7 +21,7 @@ module SourceMonitor
|
|
|
21
21
|
if recent
|
|
22
22
|
ok_result("Solid Queue workers are reporting heartbeats")
|
|
23
23
|
else
|
|
24
|
-
warning_result("No Solid Queue workers have reported in the last #{DEFAULT_HEARTBEAT_THRESHOLD.inspect}", "Start a Solid Queue worker with `bin/rails solid_queue:start`
|
|
24
|
+
warning_result("No Solid Queue workers have reported in the last #{DEFAULT_HEARTBEAT_THRESHOLD.inspect}", "Start a Solid Queue worker with `bin/rails solid_queue:start` or add `jobs: bundle exec rake solid_queue:start` to Procfile.dev and run `bin/dev`")
|
|
25
25
|
end
|
|
26
26
|
rescue StandardError => e
|
|
27
27
|
error_result("Solid Queue verification failed: #{e.message}", "Verify Solid Queue migrations are up to date and workers can access the database")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
|
+
require_relative "procfile_patcher"
|
|
5
|
+
require_relative "queue_config_patcher"
|
|
4
6
|
require_relative "skills_installer"
|
|
5
7
|
|
|
6
8
|
module SourceMonitor
|
|
@@ -35,6 +37,8 @@ module SourceMonitor
|
|
|
35
37
|
install_generator: InstallGenerator.new,
|
|
36
38
|
migration_installer: MigrationInstaller.new,
|
|
37
39
|
initializer_patcher: InitializerPatcher.new,
|
|
40
|
+
procfile_patcher: ProcfilePatcher.new,
|
|
41
|
+
queue_config_patcher: QueueConfigPatcher.new,
|
|
38
42
|
devise_detector: method(:default_devise_detector),
|
|
39
43
|
verifier: Verification::Runner.new,
|
|
40
44
|
skills_installer: SkillsInstaller.new
|
|
@@ -47,6 +51,8 @@ module SourceMonitor
|
|
|
47
51
|
@install_generator = install_generator
|
|
48
52
|
@migration_installer = migration_installer
|
|
49
53
|
@initializer_patcher = initializer_patcher
|
|
54
|
+
@procfile_patcher = procfile_patcher
|
|
55
|
+
@queue_config_patcher = queue_config_patcher
|
|
50
56
|
@devise_detector = devise_detector
|
|
51
57
|
@verifier = verifier
|
|
52
58
|
@skills_installer = skills_installer
|
|
@@ -64,6 +70,8 @@ module SourceMonitor
|
|
|
64
70
|
install_generator.run(mount_path: mount_path)
|
|
65
71
|
migration_installer.install
|
|
66
72
|
initializer_patcher.ensure_navigation_hint(mount_path: mount_path)
|
|
73
|
+
procfile_patcher.patch
|
|
74
|
+
queue_config_patcher.patch
|
|
67
75
|
|
|
68
76
|
if devise_available? && prompter.yes?("Wire Devise authentication hooks into SourceMonitor?", default: true)
|
|
69
77
|
initializer_patcher.ensure_devise_hooks
|
|
@@ -89,6 +97,8 @@ module SourceMonitor
|
|
|
89
97
|
:install_generator,
|
|
90
98
|
:migration_installer,
|
|
91
99
|
:initializer_patcher,
|
|
100
|
+
:procfile_patcher,
|
|
101
|
+
:queue_config_patcher,
|
|
92
102
|
:devise_detector,
|
|
93
103
|
:verifier,
|
|
94
104
|
:skills_installer
|