kettle-changelog 1.0.4 → 1.0.6

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: 42ae636c33d71f79fe63d7f59eaffc962a3bab7bd2fb1df78ccffaa5814806a5
4
- data.tar.gz: 698fab6d1d3a7755322fd41757f094e9f43442f3c3f87634d623c5bee703eabd
3
+ metadata.gz: 91116b5db1a2ec7a70b8bb798aae0e19222ee9eded29fc26b8bdf80e0efd0d0d
4
+ data.tar.gz: f1dd9b8ff5dc27c2393a87f4fa74c2323910248fe8765755a2a47ca74aabe105
5
5
  SHA512:
6
- metadata.gz: b85f5235a110584809417c49ca301d9e1e13997297c13cf269caccaeaf5471b58a60b32588be6be6e482a0f8d84ce856225189082915dec3802cd6b121bf9c1b
7
- data.tar.gz: 9b84ef165516207a14726dae25b8754ab9bcfb017ef458e3b0b2e60f2c90bae55c712ea02c867ee2959a25f657be8f0bd767ffd115659b31bc3fa934e1725945
6
+ metadata.gz: 383cbcaed4483a26f407bb7f9b433cddb77799f8f6c91b7f241e07c893f8ef41c2982ba071694d5197017bac689eb7c2b7afd82d6feb0d0384cdc565be37c9bf
7
+ data.tar.gz: 2b3233ceb38ae67fd12e82d03fa56590f028b5738aa5ebaaf731e6e6a071f5cc849c3ad1843535305184122cfa3478a7ca5d398312e02d726a890b78a740d9a1
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,30 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.0.6] - 2026-08-29
34
+
35
+ - TAG: [v1.0.6][1.0.6t]
36
+ - COVERAGE: 94.27% -- 1119/1187 lines in 6 files
37
+ - BRANCH COVERAGE: 77.31% -- 385/498 branches in 6 files
38
+ - 38.89% documented
39
+
40
+ ### Fixed
41
+
42
+ - Consolidate repeated keyed project-file summaries when updating a prepared release section.
43
+
44
+ - Consolidate repeated keyed project-file summaries within each published release during changelog reformatting.
45
+
46
+ ## [1.0.5] - 2026-08-28
47
+
48
+ - TAG: [v1.0.5][1.0.5t]
49
+ - COVERAGE: 94.05% -- 1075/1143 lines in 6 files
50
+ - BRANCH COVERAGE: 76.95% -- 374/486 branches in 6 files
51
+ - 39.39% documented
52
+
53
+ ### Fixed
54
+
55
+ - Backfill missing release-tag Markdown references only when the matching local Git tag exists.
56
+
33
57
  ## [1.0.4] - 2026-08-28
34
58
 
35
59
  - TAG: [v1.0.4][1.0.4t]
@@ -168,7 +192,11 @@ Please file a bug if you notice a violation of semantic versioning.
168
192
 
169
193
  - Initial release
170
194
 
171
- [Unreleased]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.4...HEAD
195
+ [Unreleased]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.6...HEAD
196
+ [1.0.6]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.5...v1.0.6
197
+ [1.0.6t]: https://github.com/kettle-dev/kettle-changelog/releases/tag/v1.0.6
198
+ [1.0.5]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.4...v1.0.5
199
+ [1.0.5t]: https://github.com/kettle-dev/kettle-changelog/releases/tag/v1.0.5
172
200
  [1.0.4]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.3...v1.0.4
173
201
  [1.0.4t]: https://github.com/kettle-dev/kettle-changelog/releases/tag/v1.0.4
174
202
  [1.0.3]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.2...v1.0.3
data/README.md CHANGED
@@ -397,7 +397,7 @@ Thanks for RTFM. ☺️
397
397
  [📌gitmoji]: https://gitmoji.dev
398
398
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
399
399
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
400
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.126-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
400
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.187-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
401
401
  [🔐security]: https://github.com/kettle-dev/kettle-changelog/blob/main/SECURITY.md
402
402
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
403
403
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -23,6 +23,8 @@ module Kettle
23
23
  LINK_REF_DEF_RE = /^\s*\[[^\]]+\]:\s+\S+/
24
24
  # Matches an ATX heading at H4 or deeper (####, #####, ...)
25
25
  DEEP_HEADING_RE = /^\#{4,}\s/
26
+ # Matches the canonical release-tag row emitted for each release section.
27
+ TAG_REFERENCE_USAGE_RE = /^ {0,3}- TAG: \[v(#{CHANGELOG_VERSION_PATTERN_SOURCE})\]\[\1t\]\s*$/o
26
28
 
27
29
  # Initialize the changelog CLI
28
30
  # Sets up paths for CHANGELOG.md and coverage.json
@@ -146,7 +148,7 @@ module Kettle
146
148
  # Keep all content prior to the link-ref (older releases and interspersed refs)
147
149
  preserved_body = after_lines[0...unreleased_ref_idx].join
148
150
  # Then append the tail starting from the Unreleased link-ref line to preserve the footer refs
149
- preserved_footer = after_lines[unreleased_ref_idx..-1].join
151
+ preserved_footer = after_lines[unreleased_ref_idx..].join
150
152
  updated << preserved_body << preserved_footer
151
153
  else
152
154
  # No Unreleased ref found; just append the remainder as-is
@@ -423,6 +425,7 @@ module Kettle
423
425
  updated = convert_heading_tag_suffix_to_list(changelog)
424
426
  updated = normalize_legacy_release_headings(updated)
425
427
  updated = remove_redundant_historical_placeholders(updated)
428
+ updated = collapse_keyed_project_file_updates_by_release(updated)
426
429
  updated = normalize_heading_spacing(updated)
427
430
  updated = ensure_footer_spacing(updated)
428
431
  updated = updated.rstrip + "\n"
@@ -458,6 +461,15 @@ module Kettle
458
461
  lines.each_with_index.reject { |_line, index| removals.any? { |range| range.cover?(index) } }.map(&:first).join
459
462
  end
460
463
 
464
+ def collapse_keyed_project_file_updates_by_release(changelog)
465
+ lines = changelog.to_s.lines
466
+ changelog_release_sections(lines).sort_by { |section| section.fetch(:start) }.reverse_each do |section|
467
+ range = section.fetch(:start)...section.fetch(:finish)
468
+ lines[range] = KeyedEntryUpserter.collapse_project_file_updates(lines[range].join).lines
469
+ end
470
+ lines.join
471
+ end
472
+
461
473
  def changelog_release_sections(lines)
462
474
  headings = lines.each_index.filter_map do |index|
463
475
  match = lines[index].match(/^## \[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\]/o)
@@ -594,7 +606,7 @@ module Kettle
594
606
  # Now next_i points to the next section heading or EOF
595
607
  before = lines[0..(start_i - 1)].join
596
608
  unreleased_body = lines[(start_i + 1)..(next_i - 1)] || []
597
- after_lines = lines[next_i..-1] || []
609
+ after_lines = lines[next_i..] || []
598
610
 
599
611
  # When this is the very first release there is no `## [X.Y.Z]` heading to act
600
612
  # as a boundary, so the footer link-ref block ([Unreleased]: ...) sits at the
@@ -603,7 +615,7 @@ module Kettle
603
615
  if next_i == lines.length
604
616
  footer_i = unreleased_body.index { |l| l.start_with?(UNRELEASED_SECTION_HEADING) }
605
617
  if footer_i
606
- after_lines = unreleased_body[footer_i..-1] + after_lines
618
+ after_lines = unreleased_body[footer_i..] + after_lines
607
619
  unreleased_body = unreleased_body[0...footer_i]
608
620
  end
609
621
  end
@@ -630,12 +642,12 @@ module Kettle
630
642
 
631
643
  prepared_version = release_heading[1]
632
644
  release_and_tail = after.lines
633
- next_release_index = release_and_tail[1..-1].to_a.index { |line| line.start_with?("## [") }
645
+ next_release_index = release_and_tail[1..].to_a.index { |line| line.start_with?("## [") }
634
646
  release_line_count = next_release_index ? next_release_index + 1 : release_and_tail.length
635
647
  release_lines = release_and_tail[0...release_line_count]
636
- tail = release_and_tail[release_line_count..-1].to_a.join
648
+ tail = release_and_tail[release_line_count..].to_a.join
637
649
 
638
- release_body = release_lines[1..-1].to_a.join
650
+ release_body = release_lines[1..].to_a.join
639
651
  merged_body = merge_release_body_with_unreleased(release_body, unreleased_block)
640
652
 
641
653
  release_section = +""
@@ -708,7 +720,8 @@ module Kettle
708
720
  end
709
721
  end
710
722
 
711
- ([leading] + sections.map { |section| section.fetch(:heading) + trim_blank_lines(section.fetch(:lines)).join }).join.rstrip + "\n"
723
+ merged = ([leading] + sections.map { |section| section.fetch(:heading) + trim_blank_lines(section.fetch(:lines)).join }).join.rstrip + "\n"
724
+ KeyedEntryUpserter.collapse_project_file_updates(merged)
712
725
  end
713
726
 
714
727
  def strip_release_metadata(release_body)
@@ -1103,7 +1116,7 @@ module Kettle
1103
1116
  scan_start = lines.index { |l| l.start_with?(UNRELEASED_SECTION_HEADING) } || lines.length
1104
1117
  t_versions = {}
1105
1118
  non_t_tag_refs = {}
1106
- lines[scan_start..-1].to_a.each do |l|
1119
+ lines[scan_start..].to_a.each do |l|
1107
1120
  # Case A: explicit tag ref key like [1.2.3t]: ...
1108
1121
  if (m = l.match(/^\[(#{CHANGELOG_VERSION_PATTERN_SOURCE})t\]:\s+(\S+)/o))
1109
1122
  t_versions[m[1]] = true
@@ -1237,6 +1250,8 @@ module Kettle
1237
1250
  end
1238
1251
 
1239
1252
  if owner && repo
1253
+ add_missing_tag_link_refs!(lines, owner, repo, except: new_version)
1254
+
1240
1255
  # Add compare link for the new version
1241
1256
  from = prev_version ? "v#{prev_version}" : detect_initial_compare_base(lines)
1242
1257
  new_compare = "[#{new_version}]: https://github.com/#{owner}/#{repo}/compare/#{from}...v#{new_version}\n"
@@ -1251,7 +1266,7 @@ module Kettle
1251
1266
  end
1252
1267
 
1253
1268
  # Rebuild and sort the reference block so Unreleased is first, then newest to oldest versions, preserving everything above first_ref
1254
- ref_lines = lines[first_ref..-1].select { |l| /^\[[^\]]+\]:\s+http/.match?(l) }
1269
+ ref_lines = lines[first_ref..].select { |l| /^\[[^\]]+\]:\s+http/.match?(l) }
1255
1270
  # Deduplicate by key (text inside the square brackets)
1256
1271
  by_key = {}
1257
1272
  ref_lines.each do |l|
@@ -1291,6 +1306,35 @@ module Kettle
1291
1306
  rebuilt.join
1292
1307
  end
1293
1308
 
1309
+ # Historical sections can contain the canonical TAG row while lacking the
1310
+ # matching Markdown reference definition. Reconcile those records rather
1311
+ # than limiting reference generation to the release currently being
1312
+ # prepared.
1313
+ def add_missing_tag_link_refs!(lines, owner, repo, except: nil)
1314
+ tag_versions = lines.filter_map do |line|
1315
+ line.match(TAG_REFERENCE_USAGE_RE)&.[](1)
1316
+ end.uniq
1317
+
1318
+ tag_versions.each do |version|
1319
+ next if version == except
1320
+ next if lines.any? { |line| line.start_with?("[#{version}t]:") }
1321
+ unless local_release_tag?(version)
1322
+ warn("kettle-changelog: local git tag v#{version} is absent; leaving its TAG reference unresolved")
1323
+ next
1324
+ end
1325
+
1326
+ lines << "[#{version}t]: https://github.com/#{owner}/#{repo}/releases/tag/v#{version}\n"
1327
+ end
1328
+ end
1329
+
1330
+ def local_release_tag?(version)
1331
+ @local_release_tags ||= begin
1332
+ output, status = Open3.capture2e("git", "tag", "--list", "v*", chdir: @root)
1333
+ status.success? ? output.lines.map(&:strip) : []
1334
+ end
1335
+ @local_release_tags.include?("v#{version}")
1336
+ end
1337
+
1294
1338
  # Ensure every Markdown atx-style heading line (e.g., "# ", "## ") has exactly one blank line
1295
1339
  # before and after it, skipping content inside fenced code blocks.
1296
1340
  def normalize_heading_spacing(text)
@@ -1335,7 +1379,7 @@ module Kettle
1335
1379
  idx = lines.index { |l| l.start_with?(UNRELEASED_SECTION_HEADING) }
1336
1380
  return text unless idx
1337
1381
  head = lines[0...idx]
1338
- tail = lines[idx..-1]
1382
+ tail = lines[idx..]
1339
1383
  # Ensure exactly one blank line between body and refs
1340
1384
  if head.any? && head.last.to_s.strip != ""
1341
1385
  head << ""
@@ -5,6 +5,67 @@ module Kettle
5
5
  class KeyedEntryUpserter
6
6
  SECTIONS = %w[Added Changed Deprecated Removed Fixed Security].freeze
7
7
  KEY_PREFIX = "[kc]"
8
+ PROJECT_FILE_UPDATE_FIRST_LINE = /\A[-*]\s+\[kc\]\s+([^\s:]+):\s+updated\s+\d+\s+project\s+files?:\s*\z/
9
+ PROJECT_FILE_UPDATE_CATEGORY_LINE = /\A\s{2,}[-*]\s+(.+?)\s+\((\d+)\)\s*\z/
10
+
11
+ class << self
12
+ # Consolidates repeated maintenance summaries after a prepared release
13
+ # is updated in place. The AST supplies list-item boundaries; the two
14
+ # regular expressions only parse the stable text payload inside those
15
+ # already-classified Markdown items.
16
+ def collapse_project_file_updates(source)
17
+ require "ast/crispr/markdown/markly"
18
+ context = Ast::Crispr::Markdown::Markly.document_context(content: source.to_s, source_label: "CHANGELOG.md")
19
+ updates = context.structural_owners(owner_scope: :list_items)
20
+ .select { |item| item.depth == 1 }
21
+ .filter_map { |item| project_file_update_record(item) }
22
+ duplicate_groups = updates.group_by { |update| update.fetch(:key) }.values.select { |group| group.length > 1 }
23
+ return source.to_s if duplicate_groups.empty?
24
+
25
+ lines = source.to_s.lines
26
+ duplicate_groups.flatten.sort_by { |update| update.fetch(:start_line) }.reverse_each do |update|
27
+ group = duplicate_groups.find { |candidate| candidate.include?(update) }
28
+ first = group.min_by { |candidate| candidate.fetch(:start_line) }
29
+ replacement = (update == first) ? project_file_update_entry(group).lines : []
30
+ lines[(update.fetch(:start_line) - 1)...update.fetch(:end_line)] = replacement
31
+ end
32
+ lines.join
33
+ end
34
+
35
+ private
36
+
37
+ def project_file_update_record(item)
38
+ lines = item.source.to_s.lines
39
+ key_match = PROJECT_FILE_UPDATE_FIRST_LINE.match(lines.shift.to_s.strip)
40
+ return unless key_match
41
+
42
+ counts = lines.filter_map do |line|
43
+ match = PROJECT_FILE_UPDATE_CATEGORY_LINE.match(line)
44
+ next unless match
45
+
46
+ [match[1].tr(" ", "_").to_sym, match[2].to_i]
47
+ end.to_h
48
+ return if counts.empty?
49
+
50
+ {
51
+ key: key_match[1],
52
+ counts: counts,
53
+ start_line: item.location.start_line,
54
+ end_line: item.location.end_line
55
+ }
56
+ end
57
+
58
+ def project_file_update_entry(entries)
59
+ counts = entries.each_with_object(Hash.new(0)) do |entry, totals|
60
+ entry.fetch(:counts).each { |category, count| totals[category] += count }
61
+ end
62
+ total = counts.values.sum
63
+ details = counts.sort_by { |category, _count| category.to_s }.map do |category, count|
64
+ " - #{category.to_s.tr("_", " ")} (#{count})"
65
+ end
66
+ ["- #{KEY_PREFIX} #{entries.first.fetch(:key)}: updated #{total} project file#{"s" unless total == 1}:", *details, ""].join("\n")
67
+ end
68
+ end
8
69
 
9
70
  def initialize(section:, key:, entry:, legacy_prefixes: [], root: Kettle::Dev::CIHelpers.project_root)
10
71
  @root = root
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "1.0.4"
8
+ VERSION = "1.0.6"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-changelog
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -63,7 +63,7 @@ dependencies:
63
63
  requirements:
64
64
  - - ">="
65
65
  - !ruby/object:Gem::Version
66
- version: 3.0.14
66
+ version: 3.0.17
67
67
  - - "<"
68
68
  - !ruby/object:Gem::Version
69
69
  version: 4.0.0
@@ -73,7 +73,7 @@ dependencies:
73
73
  requirements:
74
74
  - - ">="
75
75
  - !ruby/object:Gem::Version
76
- version: 3.0.14
76
+ version: 3.0.17
77
77
  - - "<"
78
78
  - !ruby/object:Gem::Version
79
79
  version: 4.0.0
@@ -304,10 +304,10 @@ licenses:
304
304
  - AGPL-3.0-only
305
305
  metadata:
306
306
  homepage_uri: https://kettle-changelog.galtzo.com
307
- source_code_uri: https://github.com/kettle-dev/kettle-changelog/tree/v1.0.4
308
- changelog_uri: https://github.com/kettle-dev/kettle-changelog/blob/v1.0.4/CHANGELOG.md
307
+ source_code_uri: https://github.com/kettle-dev/kettle-changelog/tree/v1.0.6
308
+ changelog_uri: https://github.com/kettle-dev/kettle-changelog/blob/v1.0.6/CHANGELOG.md
309
309
  bug_tracker_uri: https://github.com/kettle-dev/kettle-changelog/issues
310
- documentation_uri: https://www.rubydoc.info/gems/kettle-changelog/1.0.4
310
+ documentation_uri: https://www.rubydoc.info/gems/kettle-changelog/1.0.6
311
311
  funding_uri: https://github.com/sponsors/pboling
312
312
  wiki_uri: https://github.com/kettle-dev/kettle-changelog/wiki
313
313
  news_uri: https://www.railsbling.com/tags/kettle-changelog
metadata.gz.sig CHANGED
Binary file