kettle-dev 2.2.25 → 2.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e5b1b17ea5254ca2a656ec1a10a984b5f8170359507c9a7c56c46b42ab3ca0b
4
- data.tar.gz: cc447fb1871394c8473e2464fe8dd56d8b554274b1e08bdb2ae5461b92030b77
3
+ metadata.gz: 9af988c7e6584e30cd1ee4c7a799dbe13a7322e166c95a439fb131d627bb5145
4
+ data.tar.gz: 7579a10cf684681b1ae88cb9d9c2f0c0a73a1acd3a7f22f74a35277a03882e2f
5
5
  SHA512:
6
- metadata.gz: 0a0ae789338feaaae14007e02ebdb520da4d819d14783f82cf27b2d3e400f6818eefb4be5d5d485fee5cea9cd086b7387b50c0fb8d7f63e64a37214b238f6437
7
- data.tar.gz: 7a6dc01a712098bcc218652f83c219bc9e0551be7d492bec5bcd1fc0f82dbcf7b196f0fafa324a7c934c410136df41108ce22ecf449fa1d475299e3bd9a866ed
6
+ metadata.gz: 77ca7f3d5b0135b37e2f817c173cc111561d37c805103d1f34fada9b3f4cc42f8c4e47b9e45e2327436221f67918b25ff80e8f6b86b8fca2e2364318bbec059f
7
+ data.tar.gz: 82e064b7576902016b5443f2b30db1b6a2203259d51b865ea3c0eeaec2e3612b00dabcaee453ae74b7f0015066eedc47bcb2803856ba56b95f16b16ae837cfc0
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,52 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [2.3.1] - 2026-07-10
34
+
35
+ - TAG: [v2.3.1][2.3.1t]
36
+ - COVERAGE: 91.69% -- 4271/4658 lines in 36 files
37
+ - BRANCH COVERAGE: 73.08% -- 1683/2303 branches in 36 files
38
+ - 57.30% documented
39
+
40
+ ### Added
41
+
42
+ - `kettle-pre-release` now skips volatile star-history image URLs by default
43
+ during Markdown image validation, and accepts additional
44
+ `.kettle-family.yml` or `KETTLE_FAMILY_CONFIG` patterns under
45
+ `pre_release.image_url_skip_patterns`.
46
+
47
+ - `kettle-release --skip-bundle-audit` and `KETTLE_DEV_SKIP_BUNDLE_AUDIT=true`
48
+ now disable the release-flow `bundle:audit` and `bundle:audit:update` rake
49
+ defaults so a known advisory can be handled outside the automated release.
50
+
51
+ ### Changed
52
+
53
+ - `require "kettle/dev"` no longer loads `version_gem` by default; require
54
+ `kettle/dev/version_gem` for the optional `VersionGem::Basic` extension.
55
+
56
+ ### Fixed
57
+
58
+ - Legacy-engine CI workflows now install Bundler gems under
59
+ `${RUNNER_TEMP}/bundle` instead of `vendor/bundle`, and RuboCop now excludes
60
+ nested `vendor/` trees such as `gemfiles/vendor`.
61
+
62
+ - `kettle-release --skip-bundle-audit` now also prefixes release `bin/rake`
63
+ commands with `KETTLE_DEV_SKIP_BUNDLE_AUDIT=true`, ensuring the skip reaches
64
+ the rake process during release checks.
65
+
66
+ ## [2.3.0] - 2026-07-02
67
+
68
+ - TAG: [v2.3.0][2.3.0t]
69
+ - COVERAGE: 91.95% -- 4216/4585 lines in 35 files
70
+ - BRANCH COVERAGE: 73.19% -- 1660/2268 branches in 35 files
71
+ - 58.02% documented
72
+
73
+ ### Added
74
+
75
+ - `Kettle::Dev::VersionBump` now exposes the reusable engine behind
76
+ `kettle-bump`, so other tools can share version detection, target resolution,
77
+ Prism edits, and write behavior without shelling out or duplicating logic.
78
+
33
79
  ## [2.2.25] - 2026-07-01
34
80
 
35
81
  - TAG: [v2.2.25][2.2.25t]
@@ -2396,7 +2442,11 @@ Please file a bug if you notice a violation of semantic versioning.
2396
2442
  - Selecting will run the selected workflow via `act`
2397
2443
  - This may move to its own gem in the future.
2398
2444
 
2399
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.25...HEAD
2445
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.3.1...HEAD
2446
+ [2.3.1]: https://github.com/kettle-dev/kettle-dev/compare/v2.3.0...v2.3.1
2447
+ [2.3.1t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.3.1
2448
+ [2.3.0]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.25...v2.3.0
2449
+ [2.3.0t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.3.0
2400
2450
  [2.2.25]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.24...v2.2.25
2401
2451
  [2.2.25t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.2.25
2402
2452
  [2.2.24]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.23...v2.2.24
data/README.md CHANGED
@@ -496,6 +496,14 @@ What it does:
496
496
  - 1) Validate GitHub Actions workflow action refs with `kettle-gha-sha-pins --check`; if pins are stale, it prints an outdated-actions summary, exits non-zero, and recommends `kettle-gha-sha-pins --write --upgrade patch`.
497
497
  - 2) Normalize Markdown image URLs.
498
498
  - 3) Validate that all image URLs referenced by Markdown files resolve (HTTP HEAD).
499
+ - Image URL skips:
500
+ - Star-history image API URLs are skipped by default because they are volatile.
501
+ - Add project-specific shell-style URL patterns in `.kettle-family.yml` or a config path pointed to by `KETTLE_FAMILY_CONFIG`:
502
+ ```yaml
503
+ pre_release:
504
+ image_url_skip_patterns:
505
+ - https://assets.example.com/generated/*
506
+ ```
499
507
 
500
508
  ### Commit message helper (git hook)
501
509
 
@@ -874,7 +882,7 @@ Thanks for RTFM. ☺️
874
882
  [📌gitmoji]: https://gitmoji.dev
875
883
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
876
884
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
877
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.567-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
885
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.658-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
878
886
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
879
887
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
880
888
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/exe/kettle-release CHANGED
@@ -43,7 +43,7 @@ end
43
43
  # Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
44
44
  if ARGV.include?("-h") || ARGV.include?("--help")
45
45
  puts <<~USAGE
46
- Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [--skip-steps STEPS] [start_step=<0-19>]
46
+ Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [--skip-bundle-audit] [--skip-steps STEPS] [start_step=<0-19>]
47
47
 
48
48
  Automates the release flow for a Ruby gem in the host project.
49
49
 
@@ -80,6 +80,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
80
80
  --skip-steps NUMBERS # Alias for skip_steps=NUMBERS (e.g., --skip-steps 10 or --skip-steps 10,18)
81
81
  --version VERSION # Use this version instead of detecting VERSION from lib/**/version.rb
82
82
  --appraisal-update # Use slower appraisal:update instead of default appraisal:generate
83
+ --skip-bundle-audit # Skip bundle:audit/update during release rake checks
83
84
  --local-ci # Sensitive release mode: run act locally, publish before any git push,
84
85
  # create the git tag locally, then push commits and tags after publish
85
86
 
@@ -90,6 +91,8 @@ if ARGV.include?("-h") || ARGV.include?("--help")
90
91
  K_RELEASE_LOCAL_CI_WORKFLOW # Name of workflow (without .yml) for local CI; defaults to locked_deps or first
91
92
  KETTLE_RELEASE_APPRAISAL_TASK=appraisal:update
92
93
  # Use slower appraisal:update instead of default appraisal:generate
94
+ KETTLE_DEV_SKIP_BUNDLE_AUDIT=true
95
+ # Skip bundle:audit/update during release rake checks
93
96
  GITHUB_TOKEN / GH_TOKEN # Optional; enables GitHub release creation and API queries
94
97
  GITLAB_TOKEN / GL_TOKEN # Optional; enables GitLab API queries when applicable
95
98
  DEBUG=true # Print backtraces on errors
@@ -144,13 +147,14 @@ end
144
147
 
145
148
  local_ci = ARGV.include?("--local-ci")
146
149
  appraisal_task = ARGV.delete("--appraisal-update") ? "appraisal:update" : nil
150
+ skip_bundle_audit = !!ARGV.delete("--skip-bundle-audit")
147
151
  start_step_arg = ARGV.find { |a| a.start_with?("start_step=") }
148
152
  start_step = start_step_arg ? start_step_arg.split("=", 2)[1].to_i : 0
149
153
  version_override = extract_version_arg!(ARGV)
150
154
  skip_steps = extract_skip_steps_arg!(ARGV)
151
155
 
152
156
  begin
153
- Kettle::Dev::ReleaseCLI.new(start_step: start_step, local_ci: local_ci, version: version_override, appraisal_task: appraisal_task, skip_steps: skip_steps).run
157
+ Kettle::Dev::ReleaseCLI.new(start_step: start_step, local_ci: local_ci, version: version_override, appraisal_task: appraisal_task, skip_steps: skip_steps, skip_bundle_audit: skip_bundle_audit).run
154
158
  rescue LoadError => e
155
159
  warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
156
160
  warn(Array(e.backtrace).join("\n")) if ENV["DEBUG"]
@@ -6,8 +6,6 @@ module Kettle
6
6
  module Dev
7
7
  # CLI for bumping the current project's gem version before changelog prep.
8
8
  class BumpCLI
9
- BUMP_TYPES = %w[major minor patch pre].freeze
10
-
11
9
  def initialize(argv = [], out: $stdout, err: $stderr, root: Kettle::Dev::CIHelpers.project_root)
12
10
  @argv = argv.dup
13
11
  @out = out
@@ -19,16 +17,15 @@ module Kettle
19
17
  options = parse_options
20
18
  return 0 if options.fetch(:help)
21
19
 
22
- current_version = Kettle::Dev::Versioning.detect_version(root)
23
- target_version = resolve_target_version(options.fetch(:target), current_version)
24
- edits = collect_edits(
25
- current_version: current_version,
26
- target_version: target_version,
20
+ bump = Kettle::Dev::VersionBump.new(
21
+ root: root,
22
+ target_version: options.fetch(:target),
27
23
  from_version: options[:from]
28
24
  )
29
- write_edits(edits) if options.fetch(:mode) == :execute
25
+ edits = bump.edits
26
+ Kettle::Dev::VersionBump.write_edits(edits) if options.fetch(:mode) == :execute
30
27
 
31
- out.puts(summary(edits: edits, current_version: current_version, target_version: target_version, mode: options.fetch(:mode)))
28
+ out.puts(summary(edits: edits, current_version: bump.current_version, target_version: bump.target_version, mode: options.fetch(:mode)))
32
29
  (options.fetch(:mode) == :check && edits.any?) ? 1 : 0
33
30
  end
34
31
 
@@ -59,168 +56,8 @@ module Kettle
59
56
  raise Kettle::Dev::Error, error.message
60
57
  end
61
58
 
62
- def resolve_target_version(target, current_version)
63
- if BUMP_TYPES.include?(target)
64
- bumped_version(target, current_version)
65
- else
66
- validate_version(target)
67
- end
68
- end
69
-
70
- def bumped_version(type, current_version)
71
- return bumped_prerelease_version(current_version) if type == "pre"
72
-
73
- version = Gem::Version.new(current_version)
74
- segments = version.segments
75
- unless segments.all? { |segment| segment.is_a?(Integer) }
76
- raise Kettle::Dev::Error, "cannot #{type}-bump non-numeric version #{current_version.inspect}"
77
- end
78
-
79
- major, minor, patch = (segments + [0, 0, 0])[0, 3]
80
- case type
81
- when "major"
82
- "#{major + 1}.0.0"
83
- when "minor"
84
- "#{major}.#{minor + 1}.0"
85
- when "patch"
86
- "#{major}.#{minor}.#{patch + 1}"
87
- end
88
- end
89
-
90
- def bumped_prerelease_version(current_version)
91
- version = Gem::Version.new(current_version)
92
- segments = version.segments
93
- prerelease_index = segments.index { |segment| !segment.is_a?(Integer) }
94
- unless prerelease_index
95
- raise Kettle::Dev::Error, "cannot pre-bump version without prerelease segment #{current_version.inspect}"
96
- end
97
-
98
- release_core = segments[0...prerelease_index].join(".")
99
- prerelease_suffix = prerelease_suffix_for(current_version, release_core)
100
- "#{release_core}.#{prerelease_suffix.next}"
101
- end
102
-
103
- def prerelease_suffix_for(current_version, release_core)
104
- prefix = "#{release_core}."
105
- return string_tail(current_version, prefix.length) if current_version.start_with?(prefix)
106
-
107
- canonical_version = Gem::Version.new(current_version).to_s
108
- return string_tail(canonical_version, prefix.length) if canonical_version.start_with?(prefix)
109
-
110
- raise Kettle::Dev::Error, "cannot find prerelease segment in version #{current_version.inspect}"
111
- end
112
-
113
- def string_tail(value, offset)
114
- value[offset, value.length - offset]
115
- end
116
-
117
59
  def validate_version(version)
118
- Gem::Version.new(version).to_s
119
- rescue ArgumentError => error
120
- raise Kettle::Dev::Error, "invalid version #{version.inspect}: #{error.message}"
121
- end
122
-
123
- def collect_edits(current_version:, target_version:, from_version:)
124
- if from_version && current_version != from_version
125
- raise Kettle::Dev::Error, "current version is #{current_version}, not --from #{from_version}"
126
- end
127
-
128
- version_file_edits(target_version) + gemspec_version_edits(current_version, target_version)
129
- end
130
-
131
- def version_file_edits(target_version)
132
- Kettle::Dev::Versioning.version_file_candidates(root).filter_map do |path|
133
- source = File.read(path)
134
- node = version_string_node(source, path)
135
- current = node.unescaped
136
- next if current == target_version
137
-
138
- replacement = quote_like(node.location.slice, target_version)
139
- file_edit(path, source, node.location.start_offset, node.location.end_offset, replacement)
140
- end
141
- end
142
-
143
- def gemspec_version_edits(current_version, target_version)
144
- gemspec_path = gemspec_path_for_bump
145
- return [] unless gemspec_path
146
-
147
- source = File.read(gemspec_path)
148
- parse_result = parse_source(source, gemspec_path)
149
- each_node(parse_result.value).filter_map do |node|
150
- next unless node.is_a?(Prism::CallNode) && node.name == :version=
151
-
152
- version_node = node.arguments&.arguments&.first
153
- next unless version_node.is_a?(Prism::StringNode)
154
- next unless version_node.unescaped == current_version
155
- next if version_node.unescaped == target_version
156
-
157
- replacement = quote_like(version_node.location.slice, target_version)
158
- file_edit(gemspec_path, source, version_node.location.start_offset, version_node.location.end_offset, replacement)
159
- end
160
- end
161
-
162
- def gemspec_path_for_bump
163
- paths = Dir.glob(File.join(root, "*.gemspec")).sort
164
- return nil if paths.empty?
165
- raise Kettle::Dev::Error, "multiple gemspecs found; kettle-bump supports single gems only" if paths.length > 1
166
-
167
- paths.first
168
- end
169
-
170
- def version_string_node(source, path)
171
- parse_result = parse_source(source, path)
172
- constant = each_node(parse_result.value).find do |node|
173
- node.is_a?(Prism::ConstantWriteNode) && node.name == :VERSION && node.value.is_a?(Prism::StringNode)
174
- end
175
- raise Kettle::Dev::Error, "could not find string VERSION constant in #{path}" unless constant
176
-
177
- constant.value
178
- end
179
-
180
- def parse_source(source, path)
181
- require_prism
182
- parse_result = Prism.parse(source)
183
- raise Kettle::Dev::Error, "could not parse #{path}" unless parse_result.success?
184
-
185
- parse_result
186
- end
187
-
188
- def require_prism
189
- return if defined?(Prism)
190
-
191
- require "prism"
192
- rescue LoadError => error
193
- raise Kettle::Dev::Error, "kettle-bump requires Prism; install the prism gem or run on Ruby 3.3+ (#{error.message})"
194
- end
195
-
196
- def each_node(root)
197
- return enum_for(__method__, root) unless block_given?
198
-
199
- queue = [root]
200
- until queue.empty?
201
- node = queue.shift
202
- yield node
203
- queue.concat(node.child_nodes.compact) if node.respond_to?(:child_nodes)
204
- end
205
- end
206
-
207
- def file_edit(path, source, start_offset, end_offset, replacement)
208
- {path: path, source: source, start_offset: start_offset, end_offset: end_offset, replacement: replacement}
209
- end
210
-
211
- def quote_like(original, value)
212
- quote = original.start_with?("'") ? "'" : '"'
213
- "#{quote}#{value}#{quote}"
214
- end
215
-
216
- def write_edits(edits)
217
- edits.group_by { |edit| edit.fetch(:path) }.each_value do |file_edits|
218
- source = file_edits.first.fetch(:source)
219
- file_edits.sort_by { |edit| -edit.fetch(:start_offset) }.each do |edit|
220
- source[edit.fetch(:start_offset)...edit.fetch(:end_offset)] = edit.fetch(:replacement)
221
- end
222
- File.write(file_edits.first.fetch(:path), source)
223
- end
60
+ Kettle::Dev::VersionBump.validate_version(version)
224
61
  end
225
62
 
226
63
  def summary(edits:, current_version:, target_version:, mode:)
@@ -8,15 +8,17 @@ module Kettle
8
8
  FORMAT = "%t %b %c/%C"
9
9
  LENGTH = 30
10
10
 
11
- def initialize(total:, cached_title:, live_title:, output:, enabled: true)
11
+ def initialize(total:, cached_title:, live_title:, output:, enabled: true, skipped_title: nil)
12
12
  @total = total.to_i
13
13
  @cached_count = 0
14
14
  @live_count = 0
15
+ @skipped_count = 0
15
16
  @cached_bar = progress_bar(cached_title, output, enabled)
16
17
  @live_bar = progress_bar(live_title, output, enabled)
18
+ @skipped_bar = skipped_title ? progress_bar(skipped_title, output, enabled) : nil
17
19
  end
18
20
 
19
- attr_reader :cached_count, :live_count
21
+ attr_reader :cached_count, :live_count, :skipped_count
20
22
 
21
23
  def cached
22
24
  @cached_count += 1
@@ -28,6 +30,11 @@ module Kettle
28
30
  @live_bar&.increment
29
31
  end
30
32
 
33
+ def skipped
34
+ @skipped_count += 1
35
+ @skipped_bar&.increment
36
+ end
37
+
31
38
  private
32
39
 
33
40
  def progress_bar(title, output, enabled)
@@ -4,6 +4,7 @@ require "optparse"
4
4
  require "English"
5
5
  require "json"
6
6
  require "fileutils"
7
+ require "yaml"
7
8
  require "uri"
8
9
  require "net/http"
9
10
  require "openssl"
@@ -26,6 +27,10 @@ module Kettle
26
27
  # Usage: Kettle::Dev::PreReleaseCLI.new(check_num: 1).run
27
28
  class PreReleaseCLI
28
29
  IMAGE_URL_CACHE_TTL_SECONDS = 7 * 24 * 60 * 60
30
+ DEFAULT_IMAGE_URL_SKIP_PATTERNS = [
31
+ "https://api.star-history.com/svg*"
32
+ ].freeze
33
+ FAMILY_CONFIG_PATHS = [".kettle-family.yml", ".structuredmerge/kettle-family.yml"].freeze
29
34
 
30
35
  # Simple HTTP helpers for link validation
31
36
  module HTTP
@@ -271,6 +276,7 @@ module Kettle
271
276
  @check_num = 1 if @check_num < 1
272
277
  @image_url_cache_path = configured_image_url_cache_path
273
278
  @refresh_image_url_cache = env_truthy?(ENV["KETTLE_IMAGE_URL_CACHE_REFRESH"])
279
+ @image_url_skip_patterns = configured_image_url_skip_patterns
274
280
  end
275
281
 
276
282
  # Execute configured checks starting from @check_num.
@@ -359,15 +365,23 @@ module Kettle
359
365
  puts "[kettle-pre-release] Check 3: Validate Markdown image links (cached HTTP HEAD, with GET fallback)"
360
366
  urls = Markdown.extract_image_urls_from_files
361
367
  puts "[kettle-pre-release] Found #{urls.size} unique image URL(s)."
368
+ skipped = []
362
369
  cache = image_url_cache
363
370
  progress = CacheProgress.new(
364
371
  total: urls.size,
365
372
  cached_title: "Images cached",
366
373
  live_title: "Images live",
374
+ skipped_title: "Images skipped",
367
375
  output: $stdout
368
376
  )
369
377
  failures = []
370
378
  urls.each do |url|
379
+ if image_url_skipped?(url)
380
+ skipped << url
381
+ progress.skipped
382
+ next
383
+ end
384
+
371
385
  if cache && !@refresh_image_url_cache && cache.fresh_success?(url)
372
386
  progress.cached
373
387
  next
@@ -382,6 +396,7 @@ module Kettle
382
396
  end
383
397
  end
384
398
  puts "[kettle-pre-release] Image URL checks: #{progress.cached_count} cached, #{progress.live_count} live."
399
+ puts "[kettle-pre-release] Skipped #{progress.skipped_count} image URL check(s)." if skipped.any?
385
400
  if failures.any?
386
401
  warn("[kettle-pre-release] #{failures.size} image URL(s) failed validation:")
387
402
  failures.each { |u| warn(" - #{u}") }
@@ -411,6 +426,51 @@ module Kettle
411
426
  def env_truthy?(value)
412
427
  !!value.to_s.strip.match?(/\A(true|y|yes|1|on)\z/i)
413
428
  end
429
+
430
+ def configured_image_url_skip_patterns
431
+ DEFAULT_IMAGE_URL_SKIP_PATTERNS + family_configured_image_url_skip_patterns
432
+ end
433
+
434
+ def family_configured_image_url_skip_patterns
435
+ data = family_config_data
436
+ patterns = data.dig("pre_release", "image_url_skip_patterns")
437
+ Array(patterns).map(&:to_s).reject(&:empty?)
438
+ end
439
+
440
+ def family_config_data
441
+ config_path = configured_family_config_path
442
+ return {} unless config_path
443
+
444
+ data = stringify_keys(YAML.load_file(config_path) || {})
445
+ data.is_a?(Hash) ? data : {}
446
+ rescue Psych::SyntaxError, Errno::EACCES => e
447
+ warn("[kettle-pre-release] Could not read #{config_path}: #{e.class}: #{e.message}")
448
+ {}
449
+ end
450
+
451
+ def configured_family_config_path
452
+ env_path = ENV.fetch("KETTLE_FAMILY_CONFIG", nil)
453
+ return File.expand_path(env_path) if env_path && File.file?(File.expand_path(env_path))
454
+
455
+ FAMILY_CONFIG_PATHS.find { |path| File.file?(path) }
456
+ end
457
+
458
+ def image_url_skipped?(url)
459
+ @image_url_skip_patterns.any? do |pattern|
460
+ File.fnmatch?(pattern, url, File::FNM_CASEFOLD)
461
+ end
462
+ end
463
+
464
+ def stringify_keys(value)
465
+ case value
466
+ when Hash
467
+ value.to_h { |key, item| [key.to_s, stringify_keys(item)] }
468
+ when Array
469
+ value.map { |item| stringify_keys(item) }
470
+ else
471
+ value
472
+ end
473
+ end
414
474
  end
415
475
  end
416
476
  end
@@ -1,20 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Setup Bundle Audit
4
- begin
5
- require "bundler/audit/task"
3
+ skip_bundle_audit = %w[1 true yes on].include?(ENV.fetch("KETTLE_DEV_SKIP_BUNDLE_AUDIT", "").downcase)
6
4
 
7
- Bundler::Audit::Task.new
8
- Kettle::Dev.register_default("bundle:audit:update")
9
- Kettle::Dev.register_default("bundle:audit")
10
- rescue LoadError
11
- warn("[kettle-dev][bundle_audit.rake] failed to load bundle/audit/task") if Kettle::Dev::DEBUGGING
12
- desc("(stub) bundle:audit is unavailable")
5
+ define_bundle_audit_stub_tasks = lambda do |description_prefix, warning|
6
+ desc("#{description_prefix} bundle:audit")
13
7
  task("bundle:audit") do
14
- warn("NOTE: bundler-audit isn't installed, or is disabled for #{RUBY_VERSION} in the current environment")
8
+ warn(warning.call("bundle:audit"))
15
9
  end
16
- desc("(stub) bundle:audit:update is unavailable")
10
+ desc("#{description_prefix} bundle:audit:update")
17
11
  task("bundle:audit:update") do
18
- warn("NOTE: bundler-audit isn't installed, or is disabled for #{RUBY_VERSION} in the current environment")
12
+ warn(warning.call("bundle:audit:update"))
13
+ end
14
+ end
15
+
16
+ if skip_bundle_audit
17
+ define_bundle_audit_stub_tasks.call(
18
+ "(skipped)",
19
+ ->(task_name) { "NOTE: #{task_name} skipped because KETTLE_DEV_SKIP_BUNDLE_AUDIT is enabled" }
20
+ )
21
+ else
22
+ # Setup Bundle Audit
23
+ begin
24
+ require "bundler/audit/task"
25
+
26
+ Bundler::Audit::Task.new
27
+ Kettle::Dev.register_default("bundle:audit:update")
28
+ Kettle::Dev.register_default("bundle:audit")
29
+ rescue LoadError
30
+ warn("[kettle-dev][bundle_audit.rake] failed to load bundle/audit/task") if Kettle::Dev::DEBUGGING
31
+ define_bundle_audit_stub_tasks.call(
32
+ "(stub)",
33
+ ->(_task_name) { "NOTE: bundler-audit isn't installed, or is disabled for #{RUBY_VERSION} in the current environment" }
34
+ )
19
35
  end
20
36
  end
@@ -36,7 +36,12 @@ setup_spec_task = ->(default:) {
36
36
  unless Rake::Task.task_defined?(:spec)
37
37
  desc("Run RSpec code examples")
38
38
  task(:spec) do
39
- sh("bundle", "exec", "kettle-test")
39
+ begin
40
+ skip_bundle_audit = ENV.delete("KETTLE_DEV_SKIP_BUNDLE_AUDIT")
41
+ sh("bundle", "exec", "kettle-test")
42
+ ensure
43
+ ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"] = skip_bundle_audit if skip_bundle_audit
44
+ end
40
45
  end
41
46
  end
42
47
 
@@ -108,18 +108,25 @@ module Kettle
108
108
 
109
109
  public
110
110
 
111
- def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil, skip_steps: nil)
111
+ def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil, skip_steps: nil, skip_bundle_audit: nil)
112
112
  @root = Kettle::Dev::CIHelpers.project_root
113
113
  @git = Kettle::Dev::GitAdapter.new
114
114
  @start_step = (start_step || 0).to_i
115
115
  @start_step = 0 if @start_step < 0
116
116
  @skip_steps = normalize_skip_steps(skip_steps)
117
117
  @local_ci = !!local_ci
118
+ @skip_bundle_audit = truthy_value?(skip_bundle_audit) || truthy_value?(ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"])
118
119
  @version_override = Kettle::Dev::Versioning.normalize_explicit_version(version)
119
120
  @appraisal_task = normalize_appraisal_task(appraisal_task || ENV["KETTLE_RELEASE_APPRAISAL_TASK"])
120
121
  end
121
122
 
122
123
  def run
124
+ with_bundle_audit_skip_env do
125
+ run_with_release_environment
126
+ end
127
+ end
128
+
129
+ def run_with_release_environment
123
130
  run_pre_release_checks! if run_step?(0)
124
131
 
125
132
  # 1. Ensure Bundler version ✓
@@ -404,6 +411,16 @@ module Kettle
404
411
  @local_ci
405
412
  end
406
413
 
414
+ def skip_bundle_audit?
415
+ @skip_bundle_audit
416
+ end
417
+
418
+ def truthy_value?(value)
419
+ return true if value == true
420
+
421
+ DEBUG_TRUE_VALUES.include?(value.to_s.downcase)
422
+ end
423
+
407
424
  def run_step?(step)
408
425
  @start_step <= step && !@skip_steps.include?(step)
409
426
  end
@@ -701,7 +718,30 @@ module Kettle
701
718
  end
702
719
 
703
720
  def run_cmd!(cmd)
704
- self.class.run_cmd!(cmd)
721
+ cmd = bundle_audit_skip_command(cmd)
722
+ with_bundle_audit_skip_env do
723
+ self.class.run_cmd!(cmd)
724
+ end
725
+ end
726
+
727
+ def bundle_audit_skip_command(cmd)
728
+ return cmd unless skip_bundle_audit?
729
+ return cmd unless cmd.start_with?("bin/rake")
730
+ return cmd if cmd.start_with?("KETTLE_DEV_SKIP_BUNDLE_AUDIT=")
731
+
732
+ "KETTLE_DEV_SKIP_BUNDLE_AUDIT=true #{cmd}"
733
+ end
734
+
735
+ def with_bundle_audit_skip_env
736
+ return yield unless skip_bundle_audit?
737
+
738
+ previous = ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"]
739
+ ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"] = "true"
740
+ yield
741
+ ensure
742
+ if skip_bundle_audit?
743
+ previous.nil? ? ENV.delete("KETTLE_DEV_SKIP_BUNDLE_AUDIT") : ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"] = previous
744
+ end
705
745
  end
706
746
 
707
747
  def git_output(args)
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Dev
5
5
  module Version
6
- VERSION = "2.2.25"
6
+ VERSION = "2.3.1"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kettle
4
+ module Dev
5
+ # Reusable engine behind kettle-bump.
6
+ class VersionBump
7
+ BUMP_TYPES = %w[major minor patch pre].freeze
8
+
9
+ class << self
10
+ def resolve_target_version(target, current_version)
11
+ target = target.to_s
12
+ if BUMP_TYPES.include?(target)
13
+ bumped_version(target, current_version)
14
+ else
15
+ validate_version(target)
16
+ end
17
+ end
18
+
19
+ def validate_version(version)
20
+ Gem::Version.new(version).to_s
21
+ rescue ArgumentError => error
22
+ raise Kettle::Dev::Error, "invalid version #{version.inspect}: #{error.message}"
23
+ end
24
+
25
+ def bumped_version(type, current_version)
26
+ return bumped_prerelease_version(current_version) if type == "pre"
27
+
28
+ version = Gem::Version.new(current_version)
29
+ segments = version.segments
30
+ unless segments.all? { |segment| segment.is_a?(Integer) }
31
+ raise Kettle::Dev::Error, "cannot #{type}-bump non-numeric version #{current_version.inspect}"
32
+ end
33
+
34
+ major, minor, patch = (segments + [0, 0, 0])[0, 3]
35
+ case type
36
+ when "major"
37
+ "#{major + 1}.0.0"
38
+ when "minor"
39
+ "#{major}.#{minor + 1}.0"
40
+ when "patch"
41
+ "#{major}.#{minor}.#{patch + 1}"
42
+ end
43
+ end
44
+
45
+ def bumped_prerelease_version(current_version)
46
+ version = Gem::Version.new(current_version)
47
+ segments = version.segments
48
+ prerelease_index = segments.index { |segment| !segment.is_a?(Integer) }
49
+ unless prerelease_index
50
+ raise Kettle::Dev::Error, "cannot pre-bump version without prerelease segment #{current_version.inspect}"
51
+ end
52
+
53
+ release_core = segments[0...prerelease_index].join(".")
54
+ prerelease_suffix = prerelease_suffix_for(current_version, release_core)
55
+ "#{release_core}.#{prerelease_suffix.next}"
56
+ end
57
+
58
+ def prerelease_suffix_for(current_version, release_core)
59
+ prefix = "#{release_core}."
60
+ return string_tail(current_version, prefix.length) if current_version.start_with?(prefix)
61
+
62
+ canonical_version = Gem::Version.new(current_version).to_s
63
+ return string_tail(canonical_version, prefix.length) if canonical_version.start_with?(prefix)
64
+
65
+ raise Kettle::Dev::Error, "cannot find prerelease segment in version #{current_version.inspect}"
66
+ end
67
+
68
+ def string_tail(value, offset)
69
+ value[offset, value.length - offset]
70
+ end
71
+
72
+ def version_string_node(source, path)
73
+ parse_result = parse_source(source, path)
74
+ constant = each_node(parse_result.value).find do |node|
75
+ node.is_a?(Prism::ConstantWriteNode) && node.name == :VERSION && node.value.is_a?(Prism::StringNode)
76
+ end
77
+ raise Kettle::Dev::Error, "could not find string VERSION constant in #{path}" unless constant
78
+
79
+ constant.value
80
+ end
81
+
82
+ def parse_source(source, path)
83
+ require_prism
84
+ parse_result = Prism.parse(source)
85
+ raise Kettle::Dev::Error, "could not parse #{path}" unless parse_result.success?
86
+
87
+ parse_result
88
+ end
89
+
90
+ def require_prism
91
+ return if defined?(Prism)
92
+
93
+ require "prism"
94
+ rescue LoadError => error
95
+ raise Kettle::Dev::Error, "kettle-bump requires Prism; install the prism gem or run on Ruby 3.3+ (#{error.message})"
96
+ end
97
+
98
+ def each_node(root)
99
+ return enum_for(__method__, root) unless block_given?
100
+
101
+ queue = [root]
102
+ until queue.empty?
103
+ node = queue.shift
104
+ yield node
105
+ queue.concat(node.child_nodes.compact) if node.respond_to?(:child_nodes)
106
+ end
107
+ end
108
+
109
+ def file_edit(path, source, start_offset, end_offset, replacement)
110
+ {path: path, source: source, start_offset: start_offset, end_offset: end_offset, replacement: replacement}
111
+ end
112
+
113
+ def quote_like(original, value)
114
+ quote = original.start_with?("'") ? "'" : '"'
115
+ "#{quote}#{value}#{quote}"
116
+ end
117
+
118
+ def write_edits(edits)
119
+ edits.group_by { |edit| edit.fetch(:path) }.each_value do |file_edits|
120
+ source = file_edits.first.fetch(:source)
121
+ file_edits.sort_by { |edit| -edit.fetch(:start_offset) }.each do |edit|
122
+ source[edit.fetch(:start_offset)...edit.fetch(:end_offset)] = edit.fetch(:replacement)
123
+ end
124
+ File.write(file_edits.first.fetch(:path), source)
125
+ end
126
+ end
127
+ end
128
+
129
+ def initialize(root:, target_version:, current_version: nil, from_version: nil)
130
+ @root = root
131
+ @current_version = current_version || Kettle::Dev::Versioning.detect_version(root)
132
+ @target_version = self.class.resolve_target_version(target_version, @current_version)
133
+ @from_version = self.class.validate_version(from_version) if from_version
134
+ end
135
+
136
+ attr_reader :root, :current_version, :target_version, :from_version
137
+
138
+ def edits
139
+ validate_from_version
140
+ version_file_edits + gemspec_version_edits
141
+ end
142
+
143
+ def write!
144
+ self.class.write_edits(edits)
145
+ end
146
+
147
+ private
148
+
149
+ def validate_from_version
150
+ return unless from_version && current_version != from_version
151
+
152
+ raise Kettle::Dev::Error, "current version is #{current_version}, not --from #{from_version}"
153
+ end
154
+
155
+ def version_file_edits
156
+ Kettle::Dev::Versioning.version_file_candidates(root).filter_map do |path|
157
+ source = File.read(path)
158
+ node = self.class.version_string_node(source, path)
159
+ current = node.unescaped
160
+ next if current == target_version
161
+
162
+ replacement = self.class.quote_like(node.location.slice, target_version)
163
+ self.class.file_edit(path, source, node.location.start_offset, node.location.end_offset, replacement)
164
+ end
165
+ end
166
+
167
+ def gemspec_version_edits
168
+ gemspec_path = gemspec_path_for_bump
169
+ return [] unless gemspec_path
170
+
171
+ source = File.read(gemspec_path)
172
+ parse_result = self.class.parse_source(source, gemspec_path)
173
+ self.class.each_node(parse_result.value).filter_map do |node|
174
+ next unless node.is_a?(Prism::CallNode) && node.name == :version=
175
+
176
+ version_node = node.arguments&.arguments&.first
177
+ next unless version_node.is_a?(Prism::StringNode)
178
+ next unless version_node.unescaped == current_version
179
+ next if version_node.unescaped == target_version
180
+
181
+ replacement = self.class.quote_like(version_node.location.slice, target_version)
182
+ self.class.file_edit(gemspec_path, source, version_node.location.start_offset, version_node.location.end_offset, replacement)
183
+ end
184
+ end
185
+
186
+ def gemspec_path_for_bump
187
+ paths = Dir.glob(File.join(root, "*.gemspec")).sort
188
+ return nil if paths.empty?
189
+ raise Kettle::Dev::Error, "multiple gemspecs found; kettle-bump supports single gems only" if paths.length > 1
190
+
191
+ paths.first
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "version_gem"
4
+ require_relative "version"
5
+
6
+ Kettle::Dev::Version.class_eval do
7
+ extend VersionGem::Basic
8
+ end
data/lib/kettle/dev.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "version_gem"
4
3
  require_relative "dev/version"
5
4
 
6
5
  if Gem.ruby_version < Gem::Version.new("2.7")
@@ -13,9 +12,6 @@ if Gem.ruby_version < Gem::Version.new("2.7")
13
12
  end
14
13
  end
15
14
 
16
- Kettle::Dev::Version.class_eval do
17
- extend VersionGem::Basic
18
- end
19
15
  # External gems
20
16
 
21
17
  # It's not reasonable to test this ENV variable
@@ -47,6 +43,7 @@ module Kettle
47
43
  autoload :ReleaseCLI, "kettle/dev/release_cli"
48
44
  autoload :PreReleaseCLI, "kettle/dev/pre_release_cli"
49
45
  autoload :Version, "kettle/dev/version"
46
+ autoload :VersionBump, "kettle/dev/version_bump"
50
47
  autoload :Versioning, "kettle/dev/versioning"
51
48
 
52
49
  # Nested tasks namespace with autoloaded task modules
@@ -1,6 +1,10 @@
1
1
  module Kettle
2
2
  module Dev
3
3
  class PreReleaseCLI
4
+ IMAGE_URL_CACHE_TTL_SECONDS: Integer
5
+ DEFAULT_IMAGE_URL_SKIP_PATTERNS: Array[String]
6
+ FAMILY_CONFIG_PATHS: Array[String]
7
+
4
8
  module HTTP
5
9
  def self.parse_http_uri: (String url_str) -> ::URI
6
10
  def self.head_ok?: (String url_str, ?limit: Integer, ?timeout: Integer) -> bool
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-dev
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.25
4
+ version: 2.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -339,6 +339,8 @@ files:
339
339
  - lib/kettle/dev/tasks.rb
340
340
  - lib/kettle/dev/tasks/ci_task.rb
341
341
  - lib/kettle/dev/version.rb
342
+ - lib/kettle/dev/version_bump.rb
343
+ - lib/kettle/dev/version_gem.rb
342
344
  - lib/kettle/dev/versioning.rb
343
345
  - lib/kettle/emoji_regex.rb
344
346
  - sig/kettle-dev.rbs
@@ -375,10 +377,10 @@ licenses:
375
377
  - AGPL-3.0-only
376
378
  metadata:
377
379
  homepage_uri: https://kettle-dev.galtzo.com
378
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.2.25
379
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.2.25/CHANGELOG.md
380
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.3.1
381
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.3.1/CHANGELOG.md
380
382
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
381
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.2.25
383
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.3.1
382
384
  funding_uri: https://github.com/sponsors/pboling
383
385
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
384
386
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file