kettle-dev 1.1.4 ā 1.1.5
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
- checksums.yaml.gz.sig +0 -0
- data/.env.local.example +14 -1
- data/.envrc +2 -2
- data/.git-hooks/commit-msg +22 -16
- data/.git-hooks/prepare-commit-msg.example +19 -0
- data/.github/workflows/coverage.yml +2 -2
- data/.junie/guidelines.md +4 -0
- data/.opencollective.yml.example +3 -0
- data/CHANGELOG.md +31 -1
- data/CONTRIBUTING.md +29 -0
- data/FUNDING.md +2 -2
- data/README.md +148 -38
- data/README.md.example +7 -7
- data/Rakefile.example +1 -1
- data/exe/kettle-changelog +7 -2
- data/exe/kettle-commit-msg +20 -5
- data/exe/kettle-dev-setup +2 -1
- data/exe/kettle-dvcs +7 -2
- data/exe/kettle-pre-release +66 -0
- data/exe/kettle-readme-backers +7 -2
- data/exe/kettle-release +9 -2
- data/lib/kettle/dev/changelog_cli.rb +4 -5
- data/lib/kettle/dev/ci_helpers.rb +9 -5
- data/lib/kettle/dev/ci_monitor.rb +229 -8
- data/lib/kettle/dev/gem_spec_reader.rb +105 -39
- data/lib/kettle/dev/git_adapter.rb +6 -3
- data/lib/kettle/dev/git_commit_footer.rb +5 -2
- data/lib/kettle/dev/pre_release_cli.rb +248 -0
- data/lib/kettle/dev/readme_backers.rb +4 -2
- data/lib/kettle/dev/release_cli.rb +27 -17
- data/lib/kettle/dev/tasks/ci_task.rb +112 -22
- data/lib/kettle/dev/tasks/install_task.rb +23 -17
- data/lib/kettle/dev/tasks/template_task.rb +64 -23
- data/lib/kettle/dev/template_helpers.rb +44 -31
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +5 -0
- data/sig/kettle/dev/ci_monitor.rbs +6 -0
- data/sig/kettle/dev/gem_spec_reader.rbs +8 -5
- data/sig/kettle/dev/pre_release_cli.rbs +20 -0
- data/sig/kettle/dev/template_helpers.rbs +2 -0
- data/sig/kettle/dev.rbs +1 -0
- data.tar.gz.sig +0 -0
- metadata +30 -4
- metadata.gz.sig +0 -0
@@ -8,23 +8,54 @@ module Kettle
|
|
8
8
|
# Returns a Hash with all data used by this project from gemspecs.
|
9
9
|
# Cache within the process to avoid repeated loads.
|
10
10
|
class GemSpecReader
|
11
|
+
# Default minimum Ruby version to assume when a gemspec doesn't specify one.
|
12
|
+
# @return [Gem::Version]
|
11
13
|
DEFAULT_MINIMUM_RUBY = Gem::Version.new("1.8").freeze
|
12
14
|
class << self
|
13
|
-
# Load gemspec data for the project at root.
|
14
|
-
#
|
15
|
-
#
|
15
|
+
# Load gemspec data for the project at root using RubyGems.
|
16
|
+
# The reader is lenient: failures to load or missing fields are handled with defaults and warnings.
|
17
|
+
#
|
18
|
+
# @param root [String] project root containing a *.gemspec file
|
19
|
+
# @return [Hash{Symbol=>Object}] a Hash of gem metadata used by templating and tasks
|
20
|
+
# @option return [String, nil] :gemspec_path absolute path to gemspec or nil when not found
|
21
|
+
# @option return [String] :gem_name gem name ("" when not derivable)
|
22
|
+
# @option return [Gem::Version] :min_ruby minimum Ruby version derived or DEFAULT_MINIMUM_RUBY
|
23
|
+
# @option return [String] :homepage homepage string (may be "")
|
24
|
+
# @option return [String] :gh_org GitHub org (falls back to "kettle-rb")
|
25
|
+
# @option return [String] :forge_org primary forge org (currently same as gh_org)
|
26
|
+
# @option return [String, nil] :funding_org OpenCollective/org handle or nil when not discovered
|
27
|
+
# @option return [String, nil] :gh_repo GitHub repo name, if discoverable
|
28
|
+
# @option return [String] :namespace Ruby namespace derived from gem name (e.g., "Kettle::Dev")
|
29
|
+
# @option return [String] :namespace_shield URL-escaped namespace for shields
|
30
|
+
# @option return [String] :entrypoint_require require path for gem (e.g., "kettle/dev")
|
31
|
+
# @option return [String] :gem_shield shield-safe gem name
|
32
|
+
# @option return [Array<String>] :authors
|
33
|
+
# @option return [Array<String>] :email
|
34
|
+
# @option return [String] :summary
|
35
|
+
# @option return [String] :description
|
36
|
+
# @option return [Array<String>] :licenses includes both license and licenses values
|
37
|
+
# @option return [Gem::Requirement, nil] :required_ruby_version
|
38
|
+
# @option return [Array<String>] :require_paths
|
39
|
+
# @option return [String] :bindir
|
40
|
+
# @option return [Array<String>] :executables
|
16
41
|
def load(root)
|
17
42
|
gemspec_path = Dir.glob(File.join(root.to_s, "*.gemspec")).first
|
18
43
|
spec = nil
|
19
44
|
if gemspec_path && File.file?(gemspec_path)
|
20
45
|
begin
|
21
46
|
spec = Gem::Specification.load(gemspec_path)
|
22
|
-
rescue StandardError
|
47
|
+
rescue StandardError => e
|
48
|
+
Kettle::Dev.debug_error(e, __method__)
|
23
49
|
spec = nil
|
24
50
|
end
|
25
51
|
end
|
26
52
|
|
27
53
|
gem_name = spec&.name.to_s
|
54
|
+
if gem_name.nil? || gem_name.strip.empty?
|
55
|
+
# Be lenient here for tasks that can proceed without gem_name (e.g., choosing destination filenames).
|
56
|
+
Kernel.warn("kettle-dev: Could not derive gem name. Ensure a valid <name> is set in the gemspec.\n - Tip: set the gem name in your .gemspec file (spec.name).\n - Path searched: #{gemspec_path || "(none found)"}")
|
57
|
+
gem_name = ""
|
58
|
+
end
|
28
59
|
# minimum ruby version: derived from spec.required_ruby_version
|
29
60
|
# Always an instance of Gem::Version
|
30
61
|
min_ruby =
|
@@ -41,34 +72,21 @@ module Kettle
|
|
41
72
|
DEFAULT_MINIMUM_RUBY
|
42
73
|
end
|
43
74
|
rescue StandardError => e
|
75
|
+
puts "WARNING: Minimum Ruby detection failed:"
|
76
|
+
Kettle::Dev.debug_error(e, __method__)
|
44
77
|
# Default to a minimum of Ruby 1.8
|
45
|
-
puts "WARNING: Minimum Ruby detection failed: #{e.class}: #{e.message}"
|
46
78
|
DEFAULT_MINIMUM_RUBY
|
47
79
|
end
|
48
80
|
|
49
81
|
homepage_val = spec&.homepage.to_s
|
50
82
|
|
51
83
|
# Derive org/repo from homepage or git remote
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
59
|
-
end
|
60
|
-
if forge_org.nil?
|
61
|
-
begin
|
62
|
-
origin_out = IO.popen(["git", "-C", root.to_s, "remote", "get-url", "origin"], &:read)
|
63
|
-
origin_out = origin_out.read if origin_out.respond_to?(:read)
|
64
|
-
origin_url = origin_out.to_s.strip
|
65
|
-
if (m = origin_url.match(%r{github\.com[/:]([^/]+)/([^/]+)}i))
|
66
|
-
forge_org = m[1]
|
67
|
-
gh_repo = m[2].to_s.sub(/\.git\z/, "")
|
68
|
-
end
|
69
|
-
rescue StandardError
|
70
|
-
# ignore
|
71
|
-
end
|
84
|
+
forge_info = derive_forge_and_origin_repo(homepage_val)
|
85
|
+
forge_org = forge_info[:forge_org]
|
86
|
+
gh_repo = forge_info[:origin_repo]
|
87
|
+
if forge_org.to_s.empty?
|
88
|
+
Kernel.warn("kettle-dev: Could not determine forge org from spec.homepage or git remote.\n - Ensure gemspec.homepage is set to a GitHub URL or that the git remote 'origin' points to GitHub.\n - Example homepage: https://github.com/<org>/<repo>\n - Proceeding with default org: kettle-rb.")
|
89
|
+
forge_org = "kettle-rb"
|
72
90
|
end
|
73
91
|
|
74
92
|
camel = lambda do |s|
|
@@ -79,24 +97,35 @@ module Kettle
|
|
79
97
|
entrypoint_require = gem_name.to_s.tr("-", "/")
|
80
98
|
gem_shield = gem_name.to_s.gsub("-", "--").gsub("_", "__")
|
81
99
|
|
82
|
-
# Funding org detection
|
83
|
-
|
84
|
-
|
85
|
-
funding_org =
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
100
|
+
# Funding org detection with bypass support.
|
101
|
+
# By default a funding org must be discoverable, unless explicitly disabled by ENV['FUNDING_ORG'] == 'false'.
|
102
|
+
funding_org_env = ENV["FUNDING_ORG"]
|
103
|
+
funding_org = funding_org_env.to_s.strip
|
104
|
+
begin
|
105
|
+
# Handle bypass: allow explicit string 'false' (any case) to disable funding org requirement.
|
106
|
+
if funding_org_env && funding_org_env.to_s.strip.casecmp("false").zero?
|
107
|
+
funding_org = nil
|
108
|
+
else
|
109
|
+
funding_org = ENV["OPENCOLLECTIVE_HANDLE"].to_s.strip if funding_org.empty?
|
110
|
+
if funding_org.to_s.empty?
|
111
|
+
oc_path = File.join(root.to_s, ".opencollective.yml")
|
112
|
+
if File.file?(oc_path)
|
113
|
+
txt = File.read(oc_path)
|
114
|
+
if (m = txt.match(/\borg:\s*([\w\-]+)/i))
|
115
|
+
funding_org = m[1].to_s
|
116
|
+
end
|
93
117
|
end
|
94
118
|
end
|
95
|
-
|
96
|
-
|
119
|
+
# Be lenient: if funding_org cannot be determined, do not raise ā leave it nil and warn.
|
120
|
+
if funding_org.to_s.empty?
|
121
|
+
Kernel.warn("kettle-dev: Could not determine funding org.\n - Options:\n * Set ENV['FUNDING_ORG'] to your funding handle (e.g., 'opencollective-handle').\n * Or set ENV['OPENCOLLECTIVE_HANDLE'].\n * Or add .opencollective.yml with: org: <handle>\n * Or bypass by setting ENV['FUNDING_ORG']=false for gems without funding.")
|
122
|
+
funding_org = nil
|
123
|
+
end
|
97
124
|
end
|
125
|
+
rescue StandardError => error
|
126
|
+
Kettle::Dev.debug_error(error, __method__)
|
127
|
+
raise Error, "Unable to determine funding org from env or .opencollective.yml.\n\tError was: #{error.class}: #{error.message}"
|
98
128
|
end
|
99
|
-
funding_org = forge_org.to_s if funding_org.to_s.empty?
|
100
129
|
|
101
130
|
{
|
102
131
|
gemspec_path: gemspec_path,
|
@@ -123,6 +152,43 @@ module Kettle
|
|
123
152
|
executables: Array(spec&.executables),
|
124
153
|
}
|
125
154
|
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
# Derive the forge organization and origin repository name using homepage or git remotes.
|
159
|
+
# Prefers GitHub-style URLs.
|
160
|
+
#
|
161
|
+
# @param homepage_val [String] the homepage string from the gemspec (may be empty)
|
162
|
+
# @return [Hash{Symbol=>String,nil}] keys: :forge_org, :origin_repo (both may be nil when not discoverable)
|
163
|
+
def derive_forge_and_origin_repo(homepage_val)
|
164
|
+
forge_info = {}
|
165
|
+
|
166
|
+
if homepage_val && !homepage_val.empty?
|
167
|
+
m = homepage_val.match(%r{github\.com[/:]([^/]+)/([^/]+)}i)
|
168
|
+
|
169
|
+
if m
|
170
|
+
forge_info[:forge_org] = m[1]
|
171
|
+
forge_info[:origin_repo] = m[2].to_s.sub(/\.git\z/, "")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
if forge_info[:forge_org].nil? || forge_info[:forge_org].to_s.empty?
|
176
|
+
begin
|
177
|
+
ga = Kettle::Dev::GitAdapter.new
|
178
|
+
origin_url = ga.remote_url("origin") || ga.remotes_with_urls["origin"]
|
179
|
+
origin_url = origin_url.to_s.strip
|
180
|
+
if (m = origin_url.match(%r{github\.com[/:]([^/]+)/([^/]+)}i))
|
181
|
+
forge_info[:forge_org] = m[1]
|
182
|
+
forge_info[:origin_repo] = m[2].to_s.sub(/\.git\z/, "")
|
183
|
+
end
|
184
|
+
rescue StandardError => error
|
185
|
+
Kettle::Dev.debug_error(error, __method__)
|
186
|
+
# be lenient here; actual error raising will occur in caller if required
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
forge_info
|
191
|
+
end
|
126
192
|
end
|
127
193
|
end
|
128
194
|
end
|
@@ -183,7 +183,8 @@ module Kettle
|
|
183
183
|
else
|
184
184
|
system("git", "checkout", branch.to_s)
|
185
185
|
end
|
186
|
-
rescue StandardError
|
186
|
+
rescue StandardError => e
|
187
|
+
Kettle::Dev.debug_error(e, __method__)
|
187
188
|
false
|
188
189
|
end
|
189
190
|
|
@@ -198,7 +199,8 @@ module Kettle
|
|
198
199
|
else
|
199
200
|
system("git", "pull", remote.to_s, branch.to_s)
|
200
201
|
end
|
201
|
-
rescue StandardError
|
202
|
+
rescue StandardError => e
|
203
|
+
Kettle::Dev.debug_error(e, __method__)
|
202
204
|
false
|
203
205
|
end
|
204
206
|
|
@@ -219,7 +221,8 @@ module Kettle
|
|
219
221
|
else
|
220
222
|
system("git", "fetch", remote.to_s)
|
221
223
|
end
|
222
|
-
rescue StandardError
|
224
|
+
rescue StandardError => e
|
225
|
+
Kettle::Dev.debug_error(e, __method__)
|
223
226
|
false
|
224
227
|
end
|
225
228
|
end
|
@@ -22,7 +22,9 @@ module Kettle
|
|
22
22
|
begin
|
23
23
|
out = %x(git rev-parse --show-toplevel 2>/dev/null)
|
24
24
|
toplevel = out.strip unless out.nil? || out.empty?
|
25
|
-
rescue StandardError
|
25
|
+
rescue StandardError => e
|
26
|
+
Kettle::Dev.debug_error(e, __method__)
|
27
|
+
nil
|
26
28
|
end
|
27
29
|
toplevel
|
28
30
|
end
|
@@ -111,7 +113,8 @@ module Kettle
|
|
111
113
|
if @name_index
|
112
114
|
return $2
|
113
115
|
end
|
114
|
-
rescue StandardError
|
116
|
+
rescue StandardError => e
|
117
|
+
Kettle::Dev.debug_error(e, __method__)
|
115
118
|
end
|
116
119
|
nil
|
117
120
|
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "uri"
|
5
|
+
require "net/http"
|
6
|
+
require "openssl"
|
7
|
+
begin
|
8
|
+
require "addressable/uri"
|
9
|
+
rescue LoadError
|
10
|
+
# addressable is optional; code will fallback to URI
|
11
|
+
end
|
12
|
+
|
13
|
+
module Kettle
|
14
|
+
module Dev
|
15
|
+
# PreReleaseCLI: run pre-release checks before invoking full release workflow.
|
16
|
+
# Checks:
|
17
|
+
# 1) Normalize Markdown image URLs using Addressable normalization.
|
18
|
+
# 2) Validate Markdown image links resolve via HTTP(S) HEAD.
|
19
|
+
#
|
20
|
+
# Usage: Kettle::Dev::PreReleaseCLI.new(check_num: 1).run
|
21
|
+
class PreReleaseCLI
|
22
|
+
# Simple HTTP helpers for link validation
|
23
|
+
module HTTP
|
24
|
+
module_function
|
25
|
+
|
26
|
+
# Unicode-friendly HTTP URI parser with Addressable fallback.
|
27
|
+
# @param url_str [String]
|
28
|
+
# @return [URI]
|
29
|
+
def parse_http_uri(url_str)
|
30
|
+
if defined?(Addressable::URI)
|
31
|
+
addr = Addressable::URI.parse(url_str)
|
32
|
+
# Build a standard URI with properly encoded host/path/query for Net::HTTP
|
33
|
+
# Addressable handles unicode and punycode automatically via normalization
|
34
|
+
addr = addr.normalize
|
35
|
+
# Net::HTTP expects a ::URI; convert via to_s then URI.parse
|
36
|
+
URI.parse(addr.to_s)
|
37
|
+
else
|
38
|
+
# Fallback: try URI.parse directly; users can add addressable to unlock unicode support
|
39
|
+
URI.parse(url_str)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Perform HTTP HEAD against the given url.
|
44
|
+
# Falls back to GET when HEAD is not allowed.
|
45
|
+
# @param url_str [String]
|
46
|
+
# @param limit [Integer] max redirects
|
47
|
+
# @param timeout [Integer] per-request timeout seconds
|
48
|
+
# @return [Boolean] true when successful (2xx) after following redirects
|
49
|
+
def head_ok?(url_str, limit: 5, timeout: 10)
|
50
|
+
uri = parse_http_uri(url_str)
|
51
|
+
raise ArgumentError, "unsupported URI scheme: #{uri.scheme.inspect}" unless %w[http https].include?(uri.scheme)
|
52
|
+
|
53
|
+
request = Net::HTTP::Head.new(uri)
|
54
|
+
perform(uri, request, limit: limit, timeout: timeout)
|
55
|
+
end
|
56
|
+
|
57
|
+
# @api private
|
58
|
+
def perform(uri, request, limit:, timeout:)
|
59
|
+
raise ArgumentError, "too many redirects" if limit <= 0
|
60
|
+
|
61
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
62
|
+
http.use_ssl = uri.scheme == "https"
|
63
|
+
http.read_timeout = timeout
|
64
|
+
http.open_timeout = timeout
|
65
|
+
http.ssl_timeout = timeout if http.respond_to?(:ssl_timeout=)
|
66
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl?
|
67
|
+
|
68
|
+
response = http.start { |h| h.request(request) }
|
69
|
+
|
70
|
+
case response
|
71
|
+
when Net::HTTPRedirection
|
72
|
+
location = response["location"]
|
73
|
+
return false unless location
|
74
|
+
new_uri = parse_http_uri(location)
|
75
|
+
new_uri = uri + location if new_uri.relative?
|
76
|
+
head_ok?(new_uri.to_s, limit: limit - 1, timeout: timeout)
|
77
|
+
when Net::HTTPSuccess
|
78
|
+
true
|
79
|
+
else
|
80
|
+
if response.is_a?(Net::HTTPMethodNotAllowed)
|
81
|
+
get_req = Net::HTTP::Get.new(uri)
|
82
|
+
get_resp = http.start { |h| h.request(get_req) }
|
83
|
+
return get_resp.is_a?(Net::HTTPSuccess)
|
84
|
+
end
|
85
|
+
false
|
86
|
+
end
|
87
|
+
rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError, OpenSSL::SSL::SSLError => e
|
88
|
+
warn("[kettle-pre-release] HTTP error for #{uri}: #{e.class}: #{e.message}")
|
89
|
+
false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Markdown parsing helpers
|
94
|
+
module Markdown
|
95
|
+
module_function
|
96
|
+
|
97
|
+
# Extract unique remote HTTP(S) image URLs from markdown or HTML images.
|
98
|
+
# @param text [String]
|
99
|
+
# @return [Array<String>]
|
100
|
+
def extract_image_urls_from_text(text)
|
101
|
+
urls = []
|
102
|
+
|
103
|
+
# Inline image syntax
|
104
|
+
text.scan(/!\[[^\]]*\]\(([^\s)]+)(?:\s+\"[^\"]*\")?\)/) { |m| urls << m[0] }
|
105
|
+
|
106
|
+
# Reference definitions
|
107
|
+
ref_defs = {}
|
108
|
+
text.scan(/^\s*\[([^\]]+)\]:\s*(\S+)/) { |m| ref_defs[m[0]] = m[1] }
|
109
|
+
|
110
|
+
# Reference image usage
|
111
|
+
text.scan(/!\[[^\]]*\]\[([^\]]+)\]/) do |m|
|
112
|
+
id = m[0]
|
113
|
+
url = ref_defs[id]
|
114
|
+
urls << url if url
|
115
|
+
end
|
116
|
+
|
117
|
+
# HTML <img src="...">
|
118
|
+
text.scan(/<img\b[^>]*\bsrc\s*=\s*\"([^\"]+)\"[^>]*>/i) { |m| urls << m[0] }
|
119
|
+
text.scan(/<img\b[^>]*\bsrc\s*=\s*\'([^\']+)\'[^>]*>/i) { |m| urls << m[0] }
|
120
|
+
|
121
|
+
urls.reject! { |u| u.nil? || u.strip.empty? }
|
122
|
+
urls.select! { |u| u =~ %r{^https?://}i }
|
123
|
+
urls.uniq
|
124
|
+
end
|
125
|
+
|
126
|
+
# Extract from files matching glob.
|
127
|
+
# @param glob_pattern [String]
|
128
|
+
# @return [Array<String>]
|
129
|
+
def extract_image_urls_from_files(glob_pattern = "*.md")
|
130
|
+
files = Dir.glob(glob_pattern)
|
131
|
+
urls = files.flat_map do |f|
|
132
|
+
begin
|
133
|
+
extract_image_urls_from_text(File.read(f))
|
134
|
+
rescue StandardError => e
|
135
|
+
warn("[kettle-pre-release] Could not read #{f}: #{e.class}: #{e.message}")
|
136
|
+
[]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
urls.uniq
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# @param check_num [Integer] 1-based index to resume from
|
144
|
+
def initialize(check_num: 1)
|
145
|
+
@check_num = (check_num || 1).to_i
|
146
|
+
@check_num = 1 if @check_num < 1
|
147
|
+
end
|
148
|
+
|
149
|
+
# Execute configured checks starting from @check_num.
|
150
|
+
# @return [void]
|
151
|
+
def run
|
152
|
+
checks = []
|
153
|
+
checks << method(:check_markdown_uri_normalization!)
|
154
|
+
checks << method(:check_markdown_images_http!)
|
155
|
+
|
156
|
+
start = @check_num
|
157
|
+
raise ArgumentError, "check_num must be >= 1" if start < 1
|
158
|
+
begin_idx = start - 1
|
159
|
+
checks[begin_idx..-1].each_with_index do |check, i|
|
160
|
+
idx = begin_idx + i + 1
|
161
|
+
puts "[kettle-pre-release] Running check ##{idx} of #{checks.size}"
|
162
|
+
check.call
|
163
|
+
end
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
# Check 1: Normalize Markdown image URLs
|
168
|
+
# Compares URLs to Addressable-normalized form and rewrites Markdown when needed.
|
169
|
+
# @return [void]
|
170
|
+
def check_markdown_uri_normalization!
|
171
|
+
puts "[kettle-pre-release] Check 1: Normalize Markdown image URLs"
|
172
|
+
files = Dir.glob(["**/*.md", "**/*.md.example"])
|
173
|
+
changed = []
|
174
|
+
total_candidates = 0
|
175
|
+
|
176
|
+
files.each do |file|
|
177
|
+
begin
|
178
|
+
original = File.read(file)
|
179
|
+
rescue StandardError => e
|
180
|
+
warn("[kettle-pre-release] Could not read #{file}: #{e.class}: #{e.message}")
|
181
|
+
next
|
182
|
+
end
|
183
|
+
|
184
|
+
text = original.dup
|
185
|
+
urls = Markdown.extract_image_urls_from_text(text)
|
186
|
+
next if urls.empty?
|
187
|
+
|
188
|
+
total_candidates += urls.size
|
189
|
+
updated = text.dup
|
190
|
+
modified = false
|
191
|
+
|
192
|
+
urls.each do |url_str|
|
193
|
+
addr = Addressable::URI.parse(url_str)
|
194
|
+
normalized = addr.normalize.to_s
|
195
|
+
next if normalized == url_str
|
196
|
+
|
197
|
+
# Replace exact occurrences of the URL in the markdown content
|
198
|
+
updated.gsub!(url_str, normalized)
|
199
|
+
modified = true
|
200
|
+
puts " -> #{file}: normalized #{url_str} -> #{normalized}"
|
201
|
+
end
|
202
|
+
|
203
|
+
if modified && updated != original
|
204
|
+
begin
|
205
|
+
File.write(file, updated)
|
206
|
+
changed << file
|
207
|
+
rescue StandardError => e
|
208
|
+
warn("[kettle-pre-release] Could not write #{file}: #{e.class}: #{e.message}")
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
puts "[kettle-pre-release] Normalization candidates: #{total_candidates}. Files changed: #{changed.uniq.size}."
|
214
|
+
nil
|
215
|
+
end
|
216
|
+
|
217
|
+
# Check 2: Validate Markdown image links by HTTP HEAD (no rescue for parse failures)
|
218
|
+
# @return [void]
|
219
|
+
def check_markdown_images_http!
|
220
|
+
puts "[kettle-pre-release] Check 2: Validate Markdown image links (HTTP HEAD)"
|
221
|
+
urls = [
|
222
|
+
Markdown.extract_image_urls_from_files("**/*.md"),
|
223
|
+
Markdown.extract_image_urls_from_files("**/*.md.example"),
|
224
|
+
].flatten.uniq
|
225
|
+
puts "[kettle-pre-release] Found #{urls.size} unique image URL(s)."
|
226
|
+
failures = []
|
227
|
+
urls.each do |url|
|
228
|
+
print(" -> #{url} ⦠")
|
229
|
+
ok = HTTP.head_ok?(url)
|
230
|
+
if ok
|
231
|
+
puts "OK"
|
232
|
+
else
|
233
|
+
puts "FAIL"
|
234
|
+
failures << url
|
235
|
+
end
|
236
|
+
end
|
237
|
+
if failures.any?
|
238
|
+
warn("[kettle-pre-release] #{failures.size} image URL(s) failed validation:")
|
239
|
+
failures.each { |u| warn(" - #{u}") }
|
240
|
+
Kettle::Dev::ExitAdapter.abort("Image link validation failed")
|
241
|
+
else
|
242
|
+
puts "[kettle-pre-release] All image links validated."
|
243
|
+
end
|
244
|
+
nil
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -127,7 +127,8 @@ module Kettle
|
|
127
127
|
from_yml = from_yml.to_s if from_yml
|
128
128
|
return from_yml unless from_yml.nil? || from_yml.strip.empty?
|
129
129
|
end
|
130
|
-
rescue StandardError
|
130
|
+
rescue StandardError => e
|
131
|
+
Kettle::Dev.debug_error(e, __method__)
|
131
132
|
end
|
132
133
|
end
|
133
134
|
README_OSC_TAG_DEFAULT
|
@@ -321,7 +322,8 @@ module Kettle
|
|
321
322
|
from_yml = from_yml.to_s if from_yml
|
322
323
|
return from_yml unless from_yml.nil? || from_yml.strip.empty?
|
323
324
|
end
|
324
|
-
rescue StandardError
|
325
|
+
rescue StandardError => e
|
326
|
+
Kettle::Dev.debug_error(e, __method__)
|
325
327
|
end
|
326
328
|
end
|
327
329
|
COMMIT_SUBJECT_DEFAULT
|
@@ -16,6 +16,21 @@ require "ruby-progressbar"
|
|
16
16
|
module Kettle
|
17
17
|
module Dev
|
18
18
|
class ReleaseCLI
|
19
|
+
class << self
|
20
|
+
def run_cmd!(cmd)
|
21
|
+
# For Bundler-invoked build/release, explicitly prefix SKIP_GEM_SIGNING so
|
22
|
+
# the signing step is skipped even when Bundler scrubs ENV.
|
23
|
+
if ENV["SKIP_GEM_SIGNING"] && cmd =~ /\Abundle(\s+exec)?\s+rake\s+(build|release)\b/
|
24
|
+
cmd = "SKIP_GEM_SIGNING=true #{cmd}"
|
25
|
+
end
|
26
|
+
puts "$ #{cmd}"
|
27
|
+
# Pass a plain Hash for the environment to satisfy tests and avoid ENV object oddities
|
28
|
+
env_hash = ENV.respond_to?(:to_hash) ? ENV.to_hash : ENV.to_h
|
29
|
+
success = system(env_hash, cmd)
|
30
|
+
abort("Command failed: #{cmd}") unless success
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
19
34
|
private
|
20
35
|
|
21
36
|
def abort(msg)
|
@@ -243,7 +258,8 @@ module Kettle
|
|
243
258
|
version ||= detect_version
|
244
259
|
gem_name = detect_gem_name
|
245
260
|
puts "\nš Release #{gem_name} v#{version} Complete š"
|
246
|
-
rescue StandardError
|
261
|
+
rescue StandardError => e
|
262
|
+
Kettle::Dev.debug_error(e, __method__)
|
247
263
|
# Fallback if detection fails for any reason
|
248
264
|
puts "\nš Release v#{version || "unknown"} Complete š"
|
249
265
|
end
|
@@ -455,21 +471,12 @@ module Kettle
|
|
455
471
|
end
|
456
472
|
|
457
473
|
def monitor_workflows_after_push!
|
458
|
-
#
|
474
|
+
# Use abort-on-failure CI monitor to match historical behavior and specs
|
459
475
|
Kettle::Dev::CIMonitor.monitor_all!(restart_hint: "bundle exec kettle-release start_step=10")
|
460
476
|
end
|
461
477
|
|
462
478
|
def run_cmd!(cmd)
|
463
|
-
|
464
|
-
# the signing step is skipped even when Bundler scrubs ENV.
|
465
|
-
if ENV["SKIP_GEM_SIGNING"] && cmd =~ /\Abundle(\s+exec)?\s+rake\s+(build|release)\b/
|
466
|
-
cmd = "SKIP_GEM_SIGNING=true #{cmd}"
|
467
|
-
end
|
468
|
-
puts "$ #{cmd}"
|
469
|
-
# Pass a plain Hash for the environment to satisfy tests and avoid ENV object oddities
|
470
|
-
env_hash = ENV.respond_to?(:to_hash) ? ENV.to_hash : ENV.to_h
|
471
|
-
success = system(env_hash, cmd)
|
472
|
-
abort("Command failed: #{cmd}") unless success
|
479
|
+
self.class.run_cmd!(cmd)
|
473
480
|
end
|
474
481
|
|
475
482
|
def git_output(args)
|
@@ -479,15 +486,16 @@ module Kettle
|
|
479
486
|
end
|
480
487
|
|
481
488
|
def ensure_git_user!
|
482
|
-
name, ok1 = git_output([
|
483
|
-
email, ok2 = git_output([
|
489
|
+
name, ok1 = git_output(%w[config user.name])
|
490
|
+
email, ok2 = git_output(%w[config user.email])
|
484
491
|
abort("Git user.name or user.email not configured.") unless ok1 && ok2 && !name.empty? && !email.empty?
|
485
492
|
end
|
486
493
|
|
487
494
|
def ensure_bundler_2_7_plus!
|
488
495
|
begin
|
489
496
|
require "bundler"
|
490
|
-
rescue LoadError
|
497
|
+
rescue LoadError => e
|
498
|
+
Kettle::Dev.debug_error(e, __method__)
|
491
499
|
abort("Bundler is required. Please install bundler >= 2.7.0 and try again.")
|
492
500
|
end
|
493
501
|
ver = Gem::Version.new(Bundler::VERSION)
|
@@ -512,7 +520,8 @@ module Kettle
|
|
512
520
|
|
513
521
|
act_ok = begin
|
514
522
|
system("act", "--version", out: File::NULL, err: File::NULL)
|
515
|
-
rescue StandardError
|
523
|
+
rescue StandardError => e
|
524
|
+
Kettle::Dev.debug_error(e, __method__)
|
516
525
|
false
|
517
526
|
end
|
518
527
|
unless act_ok
|
@@ -592,7 +601,8 @@ module Kettle
|
|
592
601
|
series_versions = gversions.select { |gv| gv.segments[0, 2] == series }
|
593
602
|
latest_series = series_versions.last&.to_s
|
594
603
|
[latest_overall, latest_series]
|
595
|
-
rescue StandardError
|
604
|
+
rescue StandardError => e
|
605
|
+
Kettle::Dev.debug_error(e, __method__)
|
596
606
|
[nil, nil]
|
597
607
|
end
|
598
608
|
|