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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +29 -1
- data/README.md +1 -1
- data/lib/kettle/changelog/cli.rb +54 -10
- data/lib/kettle/changelog/keyed_entry_upserter.rb +61 -0
- data/lib/kettle/changelog/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +6 -6
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 91116b5db1a2ec7a70b8bb798aae0e19222ee9eded29fc26b8bdf80e0efd0d0d
|
|
4
|
+
data.tar.gz: f1dd9b8ff5dc27c2393a87f4fa74c2323910248fe8765755a2a47ca74aabe105
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
data/lib/kettle/changelog/cli.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
648
|
+
tail = release_and_tail[release_line_count..].to_a.join
|
|
637
649
|
|
|
638
|
-
release_body = release_lines[1
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
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.
|
|
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.
|
|
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.
|
|
308
|
-
changelog_uri: https://github.com/kettle-dev/kettle-changelog/blob/v1.0.
|
|
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.
|
|
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
|