kettle-dev 1.1.24 → 1.1.26
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 +20 -1
- data/README.md +14 -5
- data/README.md.example +10 -4
- data/Rakefile.example +1 -1
- data/lib/kettle/dev/readme_backers.rb +374 -34
- data/lib/kettle/dev/release_cli.rb +20 -2
- data/lib/kettle/dev/tasks/ci_task.rb +1 -1
- data/lib/kettle/dev/version.rb +1 -1
- data/sig/kettle/dev/readme_backers.rbs +1 -0
- data.tar.gz.sig +0 -0
- metadata +4 -4
- metadata.gz.sig +2 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5b1991c0a766cdf2ca22899493b377918b1c35904e8c7927e29c30ba927df02
|
4
|
+
data.tar.gz: 9ba2c426beb96666c08ad9d0462162921456d48c8c2ce2ece0d302a62ab6574e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d688be0bf828e8d633970b38b37f0cc386afeb6718ddb7578f09f22f16ddd6e25b55b24978b14e9ba0d7e90f3aad4eef36fb394680f86e34195982e8f9d81f6
|
7
|
+
data.tar.gz: eee66b8b680aea507855b8b6323769e2e9f4d297243f0ed17a84261798241749d76a565dcf18dc2ba6730717aeb7701515b353fa9520f7add4bd37f99f0924ef
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/CHANGELOG.md
CHANGED
@@ -22,14 +22,29 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
22
22
|
|
23
23
|
### Changed
|
24
24
|
|
25
|
+
- Use obfuscated URLs, and avatars from Open Collective in ReadmeBackers
|
26
|
+
|
25
27
|
### Deprecated
|
26
28
|
|
27
29
|
### Removed
|
28
30
|
|
29
31
|
### Fixed
|
30
32
|
|
33
|
+
- fixed handling of kettle-release when checksums are present and unchanged causing the gem_checksums script to fail
|
34
|
+
|
31
35
|
### Security
|
32
36
|
|
37
|
+
## [1.1.25] - 2025-09-18
|
38
|
+
|
39
|
+
- TAG: [v1.1.25][1.1.25t]
|
40
|
+
- COVERAGE: 96.87% -- 3708/3828 lines in 26 files
|
41
|
+
- BRANCH COVERAGE: 81.69% -- 1526/1868 branches in 26 files
|
42
|
+
- 78.33% documented
|
43
|
+
|
44
|
+
### Fixed
|
45
|
+
|
46
|
+
- kettle-readme-backers fails gracefully when README_UPDATER_TOKEN is missing from org secrets
|
47
|
+
|
33
48
|
## [1.1.24] - 2025-09-17
|
34
49
|
|
35
50
|
- TAG: [v1.1.24][1.1.24t]
|
@@ -969,7 +984,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
969
984
|
- Selecting will run the selected workflow via `act`
|
970
985
|
- This may move to its own gem in the future.
|
971
986
|
|
972
|
-
[Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.
|
987
|
+
[Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.26...HEAD
|
988
|
+
[1.1.26]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.25...v1.1.26
|
989
|
+
[1.1.26t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.26
|
990
|
+
[1.1.25]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.24...v1.1.25
|
991
|
+
[1.1.25t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.25
|
973
992
|
[1.1.24]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.23...v1.1.24
|
974
993
|
[1.1.24t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.24
|
975
994
|
[1.1.23]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.22...v1.1.23
|
data/README.md
CHANGED
@@ -548,6 +548,11 @@ What it does:
|
|
548
548
|
- Tip:
|
549
549
|
- Run this locally before committing to keep your README current, or schedule it in CI to refresh periodically.
|
550
550
|
- It runs automatically on a once-a-week schedule by the .github/workflows/opencollective.yml workflow that is part of the kettle-dev template.
|
551
|
+
- Authentication requirement:
|
552
|
+
- When running in CI with the provided workflow, you must provide an organization-level Actions secret named `README_UPDATER_TOKEN`.
|
553
|
+
- Create it under your GitHub organization settings: `https://github.com/organizations/<YOUR_ORG>/settings/secrets/actions`.
|
554
|
+
- The updater will look for `REPO` or `GITHUB_REPOSITORY` (both usually set by GitHub Actions) to infer `<YOUR_ORG>` for guidance.
|
555
|
+
- If `README_UPDATER_TOKEN` is missing, the tool prints a helpful error to STDERR and aborts, including a direct link to the expected org settings page.
|
551
556
|
|
552
557
|
## 🦷 FLOSS Funding
|
553
558
|
|
@@ -574,20 +579,24 @@ and [Tidelift][🏙️entsup-tidelift].
|
|
574
579
|
|
575
580
|
### Open Collective for Individuals
|
576
581
|
|
582
|
+
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/kettle-rb#backer)]
|
583
|
+
|
584
|
+
NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
|
585
|
+
|
577
586
|
<!-- OPENCOLLECTIVE-INDIVIDUALS:START -->
|
578
587
|
No backers yet. Be the first!
|
579
588
|
<!-- OPENCOLLECTIVE-INDIVIDUALS:END -->
|
580
589
|
|
581
|
-
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/kettle-rb#backer)]
|
582
|
-
|
583
590
|
### Open Collective for Organizations
|
584
591
|
|
592
|
+
Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor](https://opencollective.com/kettle-rb#sponsor)]
|
593
|
+
|
594
|
+
NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
|
595
|
+
|
585
596
|
<!-- OPENCOLLECTIVE-ORGANIZATIONS:START -->
|
586
597
|
No sponsors yet. Be the first!
|
587
598
|
<!-- OPENCOLLECTIVE-ORGANIZATIONS:END -->
|
588
599
|
|
589
|
-
Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor](https://opencollective.com/kettle-rb#sponsor)]
|
590
|
-
|
591
600
|
### Another way to support open-source
|
592
601
|
|
593
602
|
> How wonderful it is that nobody need wait a single moment before starting to improve the world.<br/>
|
@@ -916,7 +925,7 @@ Thanks for RTFM. ☺️
|
|
916
925
|
[📌gitmoji]:https://gitmoji.dev
|
917
926
|
[📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
918
927
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
919
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
928
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.225-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
920
929
|
[🔐security]: SECURITY.md
|
921
930
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
922
931
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/README.md.example
CHANGED
@@ -166,19 +166,25 @@ and [Tidelift][🏙️entsup-tidelift].
|
|
166
166
|
|
167
167
|
### Open Collective for Individuals
|
168
168
|
|
169
|
+
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/kettle-rb#backer)]
|
170
|
+
|
171
|
+
NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
|
172
|
+
|
169
173
|
<!-- OPENCOLLECTIVE-INDIVIDUALS:START -->
|
170
174
|
No backers yet. Be the first!
|
171
175
|
<!-- OPENCOLLECTIVE-INDIVIDUALS:END -->
|
172
176
|
|
173
|
-
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/kettle-rb#backer)]
|
174
|
-
|
175
177
|
### Open Collective for Organizations
|
176
178
|
|
179
|
+
Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor](https://opencollective.com/kettle-rb#sponsor)]
|
180
|
+
|
181
|
+
NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
|
182
|
+
|
177
183
|
<!-- OPENCOLLECTIVE-ORGANIZATIONS:START -->
|
178
184
|
No sponsors yet. Be the first!
|
179
185
|
<!-- OPENCOLLECTIVE-ORGANIZATIONS:END -->
|
180
186
|
|
181
|
-
|
187
|
+
[kettle-readme-backers]: https://github.com/kettle-rb/kettle-dev/blob/main/exe/kettle-readme-backers
|
182
188
|
|
183
189
|
### Another way to support open-source
|
184
190
|
|
@@ -513,7 +519,7 @@ Thanks for RTFM. ☺️
|
|
513
519
|
[📌gitmoji]:https://gitmoji.dev
|
514
520
|
[📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
515
521
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
516
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
522
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.225-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
517
523
|
[🔐security]: SECURITY.md
|
518
524
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
519
525
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/Rakefile.example
CHANGED
@@ -18,26 +18,44 @@ module Kettle
|
|
18
18
|
|
19
19
|
public
|
20
20
|
|
21
|
-
|
22
|
-
README_PATH = File.expand_path("
|
21
|
+
# Default README is the one in the current working directory of the host project
|
22
|
+
README_PATH = File.expand_path("README.md", Dir.pwd)
|
23
23
|
README_OSC_TAG_DEFAULT = "OPENCOLLECTIVE"
|
24
24
|
COMMIT_SUBJECT_DEFAULT = "💸 Thanks 🙏 to our new backers 🎒 and subscribers 📜"
|
25
25
|
# Deprecated constant maintained for backwards compatibility in tests/specs.
|
26
|
-
# Prefer OpenCollectiveConfig.yaml_path going forward.
|
27
|
-
OC_YML_PATH = OpenCollectiveConfig.yaml_path
|
26
|
+
# Prefer OpenCollectiveConfig.yaml_path going forward, but resolve to the host project root.
|
27
|
+
OC_YML_PATH = OpenCollectiveConfig.yaml_path(Dir.pwd)
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Emit a debug log line when kettle-dev debugging is enabled.
|
32
|
+
# Controlled by KETTLE_DEV_DEBUG=true (or DEBUG=true as fallback).
|
33
|
+
# @param msg [String]
|
34
|
+
# @return [void]
|
35
|
+
def debug_log(msg)
|
36
|
+
return unless Kettle::Dev::DEBUGGING
|
37
|
+
Kernel.warn("[readme_backers] #{msg}")
|
38
|
+
rescue StandardError
|
39
|
+
# never raise from a standard error within debug logging
|
40
|
+
end
|
41
|
+
|
42
|
+
public
|
28
43
|
|
29
44
|
# Ruby 2.3 compatibility: Struct keyword_init added in Ruby 2.5
|
30
45
|
# Switch to struct when dropping ruby < 2.5
|
31
46
|
# Backer = Struct.new(:name, :image, :website, :profile, keyword_init: true)
|
32
47
|
# Fallback for Ruby < 2.5 where Struct keyword_init is unsupported
|
33
48
|
class Backer
|
34
|
-
|
49
|
+
ROLE = "BACKER"
|
50
|
+
attr_accessor :name, :image, :website, :profile, :oc_type, :oc_index
|
35
51
|
|
36
|
-
def initialize(name: nil, image: nil, website: nil, profile: nil, **_ignored)
|
52
|
+
def initialize(name: nil, image: nil, website: nil, profile: nil, oc_type: nil, oc_index: nil, **_ignored)
|
37
53
|
@name = name
|
38
54
|
@image = image
|
39
55
|
@website = website
|
40
56
|
@profile = profile
|
57
|
+
@oc_type = oc_type # "backer" or "organization"
|
58
|
+
@oc_index = oc_index # Integer index within type for OC URL generation
|
41
59
|
end
|
42
60
|
end
|
43
61
|
|
@@ -46,25 +64,111 @@ module Kettle
|
|
46
64
|
@readme_path = readme_path
|
47
65
|
end
|
48
66
|
|
67
|
+
# Validate environment preconditions for running the updater.
|
68
|
+
# Ensures README_UPDATER_TOKEN is present. If missing, prints guidance and raises.
|
69
|
+
#
|
70
|
+
# @return [void]
|
71
|
+
# @raise [RuntimeError] when README_UPDATER_TOKEN is not provided
|
72
|
+
def validate
|
73
|
+
token = ENV["README_UPDATER_TOKEN"].to_s
|
74
|
+
if token.strip.empty?
|
75
|
+
repo = ENV["REPO"] || ENV["GITHUB_REPOSITORY"]
|
76
|
+
org = repo&.to_s&.split("/")&.first
|
77
|
+
org_url = if org && !org.strip.empty?
|
78
|
+
"https://github.com/organizations/#{org}/settings/secrets/actions"
|
79
|
+
else
|
80
|
+
"https://github.com/organizations/YOUR_ORG/settings/secrets/actions"
|
81
|
+
end
|
82
|
+
$stderr.puts "ERROR: README_UPDATER_TOKEN is not set."
|
83
|
+
$stderr.puts "Please create an organization-level Actions secret named README_UPDATER_TOKEN at:"
|
84
|
+
$stderr.puts " #{org_url}"
|
85
|
+
$stderr.puts "Then update the workflow to reference it, or provide README_UPDATER_TOKEN in the environment."
|
86
|
+
raise 'Missing ENV["README_UPDATER_TOKEN"]'
|
87
|
+
end
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
49
91
|
def run!
|
92
|
+
validate
|
93
|
+
debug_log("Starting run: handle=#{@handle.inspect}, readme=#{@readme_path}")
|
94
|
+
debug_log("Resolved OSC tag base=#{readme_osc_tag.inspect}")
|
50
95
|
readme = File.read(@readme_path)
|
51
96
|
|
52
97
|
# Identify previous entries for diffing/mentions
|
53
98
|
b_start, b_end = detect_backer_tags(readme)
|
54
|
-
prev_backer_identities = extract_section_identities(readme, b_start, b_end)
|
55
99
|
s_start_prev, s_end_prev = detect_sponsor_tags(readme)
|
100
|
+
debug_log("Backer tags present=#{b_start != :not_found && b_end != :not_found}; Sponsor tags present=#{s_start_prev != :not_found && s_end_prev != :not_found}")
|
101
|
+
prev_backer_identities = extract_section_identities(readme, b_start, b_end)
|
56
102
|
prev_sponsor_identities = extract_section_identities(readme, s_start_prev, s_end_prev)
|
57
103
|
|
58
|
-
#
|
59
|
-
|
104
|
+
# Fetch all BACKER-role members once and partition by tier
|
105
|
+
debug_log("Fetching OpenCollective members JSON for handle=#{@handle} ...")
|
106
|
+
raw = fetch_all_backers_raw
|
107
|
+
debug_log("Fetched #{Array(raw).size} members (role=#{Backer::ROLE}) before tier partitioning")
|
108
|
+
# Build OpenCollective type-index map to generate stable avatar/website links
|
109
|
+
index_map = build_oc_index_map(raw)
|
110
|
+
if Kettle::Dev::DEBUGGING
|
111
|
+
tier_counts = Array(raw).group_by { |h| (h["tier"] || "").to_s.strip }.transform_values(&:size)
|
112
|
+
debug_log("Tier distribution: #{tier_counts}")
|
113
|
+
empty_tier = Array(raw).select { |h| h["tier"].to_s.strip.empty? }
|
114
|
+
unless empty_tier.empty?
|
115
|
+
debug_log("Members with empty tier: count=#{empty_tier.size}; showing up to 5 samples:")
|
116
|
+
empty_tier.first(5).each_with_index do |m, i|
|
117
|
+
debug_log(" [empty-tier ##{i + 1}] name=#{m["name"].inspect}, isActive=#{m["isActive"].inspect}, profile=#{m["profile"].inspect}, website=#{m["website"].inspect}")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
other_tiers = Array(raw).map { |h| h["tier"].to_s.strip }.reject { |t| t.empty? || t.casecmp("Backer").zero? || t.casecmp("Sponsor").zero? }
|
121
|
+
unless other_tiers.empty?
|
122
|
+
counts = other_tiers.group_by { |t| t }.transform_values(&:size)
|
123
|
+
debug_log("Non-standard tiers present (excluding Backer/Sponsor): #{counts}")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
backers_hashes = Array(raw).select { |h| h["tier"].to_s.strip.casecmp("Backer").zero? }
|
127
|
+
sponsors_hashes = Array(raw).select { |h| h["tier"].to_s.strip.casecmp("Sponsor").zero? }
|
128
|
+
|
129
|
+
backers = map_hashes_to_backers(backers_hashes, index_map, force_type: "backer")
|
130
|
+
sponsors = map_hashes_to_backers(sponsors_hashes, index_map, force_type: "organization")
|
131
|
+
debug_log("Partitioned counts => Backers=#{backers.size}, Sponsors=#{sponsors.size}")
|
132
|
+
if Kettle::Dev::DEBUGGING && backers.empty? && sponsors.empty? && Array(raw).any?
|
133
|
+
debug_log("No Backer or Sponsor tiers matched among #{Array(raw).size} BACKER-role records. If tiers are empty, they will not appear in Backers/Sponsors sections.")
|
134
|
+
end
|
135
|
+
|
136
|
+
# Additional dynamic tiers (exclude Backer/Sponsor)
|
137
|
+
extra_map = {}
|
138
|
+
Array(raw).group_by { |h| h["tier"].to_s.strip }.each do |tier, members|
|
139
|
+
normalized = tier.empty? ? "Donors" : tier
|
140
|
+
next if normalized.casecmp("Backer").zero? || normalized.casecmp("Sponsor").zero?
|
141
|
+
extra_map[normalized] = map_hashes_to_backers(members, index_map)
|
142
|
+
end
|
143
|
+
debug_log("Extra tiers detected: #{extra_map.keys.sort}") unless extra_map.empty?
|
144
|
+
|
60
145
|
backers_md = generate_markdown(backers, empty_message: "No backers yet. Be the first!", default_name: "Backer")
|
61
|
-
|
146
|
+
sponsors_md_base = generate_markdown(sponsors, empty_message: "No sponsors yet. Be the first!", default_name: "Sponsor")
|
147
|
+
|
148
|
+
extra_tiers_md = generate_extra_tiers_markdown(extra_map)
|
149
|
+
sponsors_md = if extra_tiers_md.empty?
|
150
|
+
sponsors_md_base
|
151
|
+
else
|
152
|
+
[sponsors_md_base, "", extra_tiers_md].join("\n")
|
153
|
+
end
|
154
|
+
|
155
|
+
# Update backers section
|
156
|
+
# If the identities in the existing block match the identities derived from current data,
|
157
|
+
# treat as no-change to avoid rewriting formatting (e.g., Markdown -> HTML OC anchors).
|
158
|
+
semantically_same_backers = semantically_same_section?(prev_backer_identities, backers)
|
159
|
+
updated = if semantically_same_backers
|
160
|
+
:no_change
|
161
|
+
else
|
162
|
+
replace_between_tags(readme, b_start, b_end, backers_md)
|
163
|
+
end
|
62
164
|
case updated
|
63
165
|
when :not_found
|
166
|
+
debug_log("Backers tag block not found; skipping backers section update")
|
64
167
|
updated_readme = readme
|
65
168
|
backers_changed = false
|
66
169
|
new_backers = []
|
67
170
|
when :no_change
|
171
|
+
debug_log("Backers section unchanged (identities match or generated markdown matches existing block)")
|
68
172
|
updated_readme = readme
|
69
173
|
backers_changed = false
|
70
174
|
new_backers = []
|
@@ -72,19 +176,35 @@ module Kettle
|
|
72
176
|
updated_readme = updated
|
73
177
|
backers_changed = true
|
74
178
|
new_backers = compute_new_members(prev_backer_identities, backers)
|
179
|
+
debug_log("Backers section updated; new_backers=#{new_backers.size}")
|
75
180
|
end
|
76
181
|
|
77
|
-
#
|
78
|
-
sponsors = fetch_members("sponsors.json")
|
79
|
-
sponsors_md = generate_markdown(sponsors, empty_message: "No sponsors yet. Be the first!", default_name: "Sponsor")
|
182
|
+
# Update sponsors section (with extra tiers appended when present)
|
80
183
|
s_start, s_end = detect_sponsor_tags(updated_readme)
|
81
|
-
|
184
|
+
# If there is no sponsors section but there is a backers section, append extra tiers to backers instead.
|
185
|
+
if s_start == :not_found && !extra_tiers_md.empty? && b_start != :not_found
|
186
|
+
debug_log("Sponsors tags not found; appending extra tiers under Backers section")
|
187
|
+
backers_md_with_extra = [backers_md, "", extra_tiers_md].join("\n")
|
188
|
+
updated = replace_between_tags(updated_readme, b_start, b_end, backers_md_with_extra)
|
189
|
+
updated_readme = updated unless updated == :no_change || updated == :not_found
|
190
|
+
end
|
191
|
+
|
192
|
+
# Sponsors: apply the same semantic no-change rule
|
193
|
+
prev_s_ids = extract_section_identities(updated_readme, s_start, s_end)
|
194
|
+
semantically_same_sponsors = semantically_same_section?(prev_s_ids, sponsors)
|
195
|
+
updated2 = if semantically_same_sponsors
|
196
|
+
:no_change
|
197
|
+
else
|
198
|
+
replace_between_tags(updated_readme, s_start, s_end, sponsors_md)
|
199
|
+
end
|
82
200
|
case updated2
|
83
201
|
when :not_found
|
202
|
+
debug_log("Sponsors tag block not found; skipping sponsors section update")
|
84
203
|
sponsors_changed = false
|
85
204
|
final = updated_readme
|
86
205
|
new_sponsors = []
|
87
206
|
when :no_change
|
207
|
+
debug_log("Sponsors section unchanged (identities match or generated markdown matches existing block)")
|
88
208
|
sponsors_changed = false
|
89
209
|
final = updated_readme
|
90
210
|
new_sponsors = []
|
@@ -92,6 +212,7 @@ module Kettle
|
|
92
212
|
sponsors_changed = true
|
93
213
|
final = updated2
|
94
214
|
new_sponsors = compute_new_members(prev_sponsor_identities, sponsors)
|
215
|
+
debug_log("Sponsors section updated; new_sponsors=#{new_sponsors.size}")
|
95
216
|
end
|
96
217
|
|
97
218
|
if !backers_changed && !sponsors_changed
|
@@ -99,9 +220,11 @@ module Kettle
|
|
99
220
|
ts = tag_strings
|
100
221
|
warn("No recognized Open Collective tags found in #{@readme_path}. Expected one or more of: " \
|
101
222
|
"#{ts[:generic_start]}/#{ts[:generic_end]}, #{ts[:individuals_start]}/#{ts[:individuals_end]}, #{ts[:orgs_start]}/#{ts[:orgs_end]}.")
|
223
|
+
debug_log("Missing tags: looked for #{ts}")
|
102
224
|
# Do not exit the process during tests or library use; just return.
|
103
225
|
return
|
104
226
|
end
|
227
|
+
debug_log("No changes detected after processing; Backers=#{backers.size}, Sponsors=#{sponsors.size}, ExtraTiers=#{extra_map.keys.size}")
|
105
228
|
puts "No changes to backers or sponsors sections in #{@readme_path}."
|
106
229
|
return
|
107
230
|
end
|
@@ -122,9 +245,9 @@ module Kettle
|
|
122
245
|
env = ENV["KETTLE_DEV_BACKER_README_OSC_TAG"].to_s
|
123
246
|
return env unless env.strip.empty?
|
124
247
|
|
125
|
-
if File.file?(
|
248
|
+
if File.file?(OC_YML_PATH)
|
126
249
|
begin
|
127
|
-
yml = YAML.safe_load(File.read(
|
250
|
+
yml = YAML.safe_load(File.read(OC_YML_PATH))
|
128
251
|
if yml.is_a?(Hash)
|
129
252
|
from_yml = yml["readme-osc-tag"] || yml[:"readme-osc-tag"]
|
130
253
|
from_yml = from_yml.to_s if from_yml
|
@@ -150,11 +273,13 @@ module Kettle
|
|
150
273
|
end
|
151
274
|
|
152
275
|
def resolve_handle
|
153
|
-
OpenCollectiveConfig.handle(required: true)
|
276
|
+
OpenCollectiveConfig.handle(required: true, root: Dir.pwd)
|
154
277
|
end
|
155
278
|
|
156
|
-
def
|
157
|
-
|
279
|
+
def fetch_all_backers_raw
|
280
|
+
api_path = "members/all.json"
|
281
|
+
url = URI("https://opencollective.com/#{@handle}/#{api_path}")
|
282
|
+
debug_log("GET #{url}")
|
158
283
|
response = Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == "https") do |conn|
|
159
284
|
conn.read_timeout = 10
|
160
285
|
conn.open_timeout = 5
|
@@ -162,36 +287,169 @@ module Kettle
|
|
162
287
|
req["User-Agent"] = "kettle-dev/README-backers"
|
163
288
|
conn.request(req)
|
164
289
|
end
|
165
|
-
|
290
|
+
unless response.is_a?(Net::HTTPSuccess)
|
291
|
+
body_len = (response.respond_to?(:body) && response.body) ? response.body.bytesize : 0
|
292
|
+
code = response.respond_to?(:code) ? response.code : response.class.name
|
293
|
+
warn("OpenCollective API non-success for #{api_path}: status=#{code}, body_len=#{body_len}")
|
294
|
+
debug_log("Response body (truncated 500 bytes): #{response.body.to_s[0, 500]}") if Kettle::Dev::DEBUGGING && body_len.to_i > 0
|
295
|
+
return []
|
296
|
+
end
|
166
297
|
|
167
298
|
parsed = JSON.parse(response.body)
|
168
|
-
Array(parsed)
|
299
|
+
all = Array(parsed)
|
300
|
+
filtered = all.select { |h| h["role"].to_s.upcase == Backer::ROLE }
|
301
|
+
debug_log("Parsed #{all.size} records; filtered BACKER => #{filtered.size}")
|
302
|
+
filtered
|
303
|
+
rescue JSON::ParserError => e
|
304
|
+
warn("Error parsing #{api_path} JSON: #{e.message}")
|
305
|
+
debug_log("Body that failed to parse (truncated 500): #{response&.body.to_s[0, 500]}")
|
306
|
+
[]
|
307
|
+
rescue StandardError => e
|
308
|
+
warn("Error fetching #{api_path}: #{e.class}: #{e.message}")
|
309
|
+
debug_log(e.backtrace.join("\n"))
|
310
|
+
[]
|
311
|
+
end
|
312
|
+
|
313
|
+
# Build a deterministic OpenCollective index map used to construct avatar/website URLs.
|
314
|
+
# Rules:
|
315
|
+
# - Iterate through the raw BACKER-role array in order received from OC.
|
316
|
+
# - Maintain two independent counters: one for "backer" (users) and one for "organization".
|
317
|
+
# - Derive a stable identity key for each member by preferring profile URL, then website URL, then name; all downcased.
|
318
|
+
# - Assign the current counter as the index for that type and increment the counter.
|
319
|
+
# - Return a Hash mapping the identity key to { type: "backer"|"organization", index: Integer }.
|
320
|
+
# This lets generate_markdown output links like:
|
321
|
+
# https://opencollective.com/<handle>/<type>/<index>/website and avatar.svg
|
322
|
+
def build_oc_index_map(hashes)
|
323
|
+
counts = {"backer" => 0, "organization" => 0}
|
324
|
+
map = {}
|
325
|
+
Array(hashes).each do |h|
|
326
|
+
type = (h["type"].to_s.upcase == "ORGANIZATION") ? "organization" : "backer"
|
327
|
+
key = if h["profile"].to_s.strip != ""
|
328
|
+
h["profile"].to_s.strip.downcase
|
329
|
+
elsif h["website"].to_s.strip != ""
|
330
|
+
h["website"].to_s.strip.downcase
|
331
|
+
else
|
332
|
+
h["name"].to_s.strip.downcase
|
333
|
+
end
|
334
|
+
idx = counts[type]
|
335
|
+
counts[type] = idx + 1
|
336
|
+
map[key] = {type: type, index: idx}
|
337
|
+
end
|
338
|
+
# Helpful debug summary so users can see which index maps to which backer.
|
339
|
+
if Kettle::Dev::DEBUGGING
|
340
|
+
samples = map.first(5).map { |k, v| "#{v[:type]}##{v[:index]} => #{k}" }
|
341
|
+
debug_log("Built OC index map: backer_count=#{counts["backer"]}, organization_count=#{counts["organization"]}; sample=#{samples}")
|
342
|
+
end
|
343
|
+
map
|
344
|
+
end
|
345
|
+
|
346
|
+
def map_hashes_to_backers(hashes, index_map = nil, force_type: nil)
|
347
|
+
forced_counter = 0
|
348
|
+
Array(hashes).map do |h|
|
349
|
+
key = if h["profile"].to_s.strip != ""
|
350
|
+
h["profile"].to_s.strip.downcase
|
351
|
+
elsif h["website"].to_s.strip != ""
|
352
|
+
h["website"].to_s.strip.downcase
|
353
|
+
else
|
354
|
+
h["name"].to_s.strip.downcase
|
355
|
+
end
|
356
|
+
oc = index_map ? index_map[key] : nil
|
357
|
+
# Determine oc_type/index with optional forced type override (used for sections)
|
358
|
+
oc_type = nil
|
359
|
+
oc_index = nil
|
360
|
+
if force_type
|
361
|
+
if oc && oc[:type] == force_type && !oc[:index].nil?
|
362
|
+
oc_type = oc[:type]
|
363
|
+
oc_index = oc[:index]
|
364
|
+
else
|
365
|
+
oc_type = force_type
|
366
|
+
oc_index = forced_counter
|
367
|
+
forced_counter += 1
|
368
|
+
end
|
369
|
+
else
|
370
|
+
oc_type = oc ? oc[:type] : nil
|
371
|
+
oc_index = oc ? oc[:index] : nil
|
372
|
+
end
|
169
373
|
Backer.new(
|
170
374
|
name: h["name"],
|
171
|
-
image:
|
375
|
+
image: begin
|
376
|
+
# Prefer OpenCollective's "avatar" key; fallback to legacy "image"
|
377
|
+
img = h["avatar"]
|
378
|
+
img = h["image"] if img.to_s.strip.empty?
|
379
|
+
img.to_s.strip.empty? ? nil : img
|
380
|
+
end,
|
172
381
|
website: (h["website"].to_s.strip.empty? ? nil : h["website"]),
|
173
382
|
profile: (h["profile"].to_s.strip.empty? ? nil : h["profile"]),
|
383
|
+
oc_type: oc_type,
|
384
|
+
oc_index: oc_index,
|
174
385
|
)
|
175
386
|
end
|
176
|
-
rescue JSON::ParserError => e
|
177
|
-
warn("Error parsing #{path} JSON: #{e.message}")
|
178
|
-
[]
|
179
|
-
rescue StandardError => e
|
180
|
-
warn("Error fetching #{path}: #{e.class}: #{e.message}")
|
181
|
-
[]
|
182
387
|
end
|
183
388
|
|
184
389
|
def generate_markdown(members, empty_message:, default_name:)
|
185
390
|
return empty_message if members.nil? || members.empty?
|
186
391
|
|
187
392
|
members.map do |m|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
393
|
+
# Prefer deterministic OpenCollective avatar/link form when index is available
|
394
|
+
if m.oc_type && !m.oc_type.to_s.strip.empty? && !m.oc_index.nil?
|
395
|
+
type = m.oc_type
|
396
|
+
idx = m.oc_index
|
397
|
+
href = "https://opencollective.com/#{@handle}/#{type}/#{idx}/website"
|
398
|
+
img = "https://opencollective.com/#{@handle}/#{type}/#{idx}/avatar.svg"
|
399
|
+
%(<a href="#{href}" target="_blank"><img src="#{img}"></a>)
|
400
|
+
else
|
401
|
+
# Fallback to prior Markdown behavior
|
402
|
+
image_url = (m.image && !m.image.to_s.strip.empty?) ? m.image : nil
|
403
|
+
primary_link = (m.website && !m.website.to_s.strip.empty?) ? m.website : nil
|
404
|
+
fallback_link = (m.profile && !m.profile.to_s.strip.empty?) ? m.profile : nil
|
405
|
+
link = primary_link || fallback_link || "#"
|
406
|
+
name = (m.name && !m.name.strip.empty?) ? m.name : default_name
|
407
|
+
if image_url
|
408
|
+
"[](#{link})"
|
409
|
+
else
|
410
|
+
"[#{escape_text(name)}](#{link})"
|
411
|
+
end
|
412
|
+
end
|
192
413
|
end.join(" ")
|
193
414
|
end
|
194
415
|
|
416
|
+
# Build markdown for any additional tiers beyond Backer/Sponsor.
|
417
|
+
# Accepts a Hash of { tier_name => [Backer, ...] }.
|
418
|
+
# Returns an empty string when there are no extra tiers.
|
419
|
+
def generate_extra_tiers_markdown(extra_map)
|
420
|
+
return "" if extra_map.nil? || extra_map.empty?
|
421
|
+
|
422
|
+
blocks = []
|
423
|
+
extra_map.keys.sort.each do |tier|
|
424
|
+
members = extra_map[tier]
|
425
|
+
next if members.nil? || members.empty?
|
426
|
+
# For extra tiers, render using plain Markdown links to satisfy specs
|
427
|
+
# that expect either Markdown or a bare <a> tag (without nested <img>).
|
428
|
+
members_plain = Array(members).map do |m|
|
429
|
+
Backer.new(
|
430
|
+
name: m.name,
|
431
|
+
image: m.image,
|
432
|
+
website: m.website,
|
433
|
+
profile: m.profile,
|
434
|
+
)
|
435
|
+
end
|
436
|
+
# Build a single, well-formed block per tier with deterministic spacing:
|
437
|
+
# - Header
|
438
|
+
# - One empty line
|
439
|
+
# - Links line
|
440
|
+
block = [
|
441
|
+
"### Open Collective for #{tier}",
|
442
|
+
"",
|
443
|
+
generate_markdown(members_plain, empty_message: "", default_name: tier),
|
444
|
+
].join("\n")
|
445
|
+
blocks << block
|
446
|
+
end
|
447
|
+
# Separate multiple tiers with a single blank line between blocks.
|
448
|
+
# The caller (replace_between_tags) will append one trailing newline before the end tag,
|
449
|
+
# yielding exactly two newlines after the links line within the section.
|
450
|
+
blocks.join("\n\n")
|
451
|
+
end
|
452
|
+
|
195
453
|
def replace_between_tags(content, start_tag, end_tag, new_content)
|
196
454
|
return :not_found if start_tag == :not_found || end_tag == :not_found
|
197
455
|
|
@@ -201,11 +459,14 @@ module Kettle
|
|
201
459
|
|
202
460
|
before = content[0..start_index + start_tag.length - 1]
|
203
461
|
after = content[end_index..-1]
|
204
|
-
replacement = "#{start_tag}\n#{new_content}\n#{end_tag}"
|
205
462
|
current_block = content[start_index..end_index + end_tag.length - 1]
|
206
|
-
|
463
|
+
# Treat as unchanged if the block matches either single or double newline spacing
|
464
|
+
unchanged_a = "#{start_tag}\n#{new_content}\n#{end_tag}"
|
465
|
+
unchanged_b = "#{start_tag}\n#{new_content}\n\n#{end_tag}"
|
466
|
+
return :no_change if current_block == unchanged_a || current_block == unchanged_b
|
207
467
|
|
208
468
|
trailing = after[end_tag.length..-1] || ""
|
469
|
+
# Use a single blank line between content and end tag for normalized output
|
209
470
|
"#{before}\n#{new_content}\n#{end_tag}#{trailing}"
|
210
471
|
end
|
211
472
|
|
@@ -238,14 +499,28 @@ module Kettle
|
|
238
499
|
|
239
500
|
block = content[(start_index + start_tag.length)...end_index]
|
240
501
|
identities = Set.new
|
502
|
+
# 1) Image-style link wrappers: [](HREF)
|
241
503
|
block.to_s.scan(/\[!\[[^\]]*\]\([^\)]*\)\]\(([^\)]+)\)/) do |m|
|
242
504
|
href = (m[0] || "").strip
|
243
505
|
identities << href.downcase unless href.empty?
|
244
506
|
end
|
507
|
+
# 2) Capture ALT text from image-style wrappers for name identity
|
245
508
|
block.to_s.scan(/\[!\[([^\]]*)\]\([^\)]*\)\]\([^\)]*\)/) do |m|
|
246
509
|
alt = (m[0] || "").strip
|
247
510
|
identities << alt.downcase unless alt.empty?
|
248
511
|
end
|
512
|
+
# 3) Plain markdown links: [TEXT](HREF)
|
513
|
+
block.to_s.scan(/\[([^!][^\]]*)\]\(([^\)]+)\)/) do |m|
|
514
|
+
text = (m[0] || "").strip
|
515
|
+
href = (m[1] || "").strip
|
516
|
+
identities << href.downcase unless href.empty?
|
517
|
+
identities << text.downcase unless text.empty?
|
518
|
+
end
|
519
|
+
# 4) HTML anchors: <a href="HREF">...</a>
|
520
|
+
block.to_s.scan(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>/i) do |m|
|
521
|
+
href = (m[0] || "").strip
|
522
|
+
identities << href.downcase unless href.empty?
|
523
|
+
end
|
249
524
|
identities
|
250
525
|
end
|
251
526
|
|
@@ -257,6 +532,71 @@ module Kettle
|
|
257
532
|
end
|
258
533
|
end
|
259
534
|
|
535
|
+
# Build the identity set for a list of members using the same precedence
|
536
|
+
# as compute_new_members/identity_for_member. Used to compare semantic
|
537
|
+
# equivalence of existing README sections vs new data, to avoid rewrites
|
538
|
+
# when only formatting differs.
|
539
|
+
# @param members [Array<Backer>]
|
540
|
+
# @return [Set<String>]
|
541
|
+
def identities_for_members(members)
|
542
|
+
set = Set.new
|
543
|
+
Array(members).each do |m|
|
544
|
+
ids = []
|
545
|
+
# Include deterministic OpenCollective href identity when available
|
546
|
+
if m.oc_type && !m.oc_type.to_s.strip.empty? && !m.oc_index.nil?
|
547
|
+
ids << "https://opencollective.com/#{@handle}/#{m.oc_type}/#{m.oc_index}/website"
|
548
|
+
end
|
549
|
+
# Also include the standard identity used elsewhere (profile/website/name)
|
550
|
+
ids << identity_for_member(m)
|
551
|
+
ids.compact.each do |id|
|
552
|
+
norm = id.to_s.strip.downcase
|
553
|
+
set << norm unless norm.empty?
|
554
|
+
end
|
555
|
+
end
|
556
|
+
set
|
557
|
+
end
|
558
|
+
|
559
|
+
# Determine if the new identity set is semantically the same as the previous
|
560
|
+
# block found in the README. We treat it as the same when all new identities
|
561
|
+
# are already present in the previous set (subset), allowing the previous set
|
562
|
+
# to contain additional identities such as plain-text names extracted from
|
563
|
+
# Markdown ALT text. This avoids unnecessary rewrites when only formatting
|
564
|
+
# changes (e.g., Markdown -> HTML OC anchors) but the underlying members are
|
565
|
+
# unchanged.
|
566
|
+
# @param previous [Set<String>]
|
567
|
+
# @param new_set [Set<String>]
|
568
|
+
# @return [Boolean]
|
569
|
+
def identities_semantically_same?(previous, new_set)
|
570
|
+
return false if new_set.nil? || new_set.empty?
|
571
|
+
return false if previous.nil? || previous.empty?
|
572
|
+
new_set.all? { |id| previous.include?(id) }
|
573
|
+
end
|
574
|
+
|
575
|
+
# Determine semantic sameness by ensuring each member has at least one
|
576
|
+
# identity present in the previous README identities set. This allows
|
577
|
+
# different formatting (OC anchors vs Markdown website/profile links)
|
578
|
+
# without rewriting when the underlying members are the same.
|
579
|
+
# @param previous [Set<String>]
|
580
|
+
# @param members [Array<Backer>]
|
581
|
+
# @return [Boolean]
|
582
|
+
def semantically_same_section?(previous, members)
|
583
|
+
return false if previous.nil? || previous.empty?
|
584
|
+
Array(members).all? do |m|
|
585
|
+
candidates = []
|
586
|
+
if m.oc_type && !m.oc_type.to_s.strip.empty? && !m.oc_index.nil?
|
587
|
+
candidates << "https://opencollective.com/#{@handle}/#{m.oc_type}/#{m.oc_index}/website"
|
588
|
+
end
|
589
|
+
candidates << m.profile
|
590
|
+
candidates << m.website
|
591
|
+
candidates << m.name
|
592
|
+
candidates.compact!
|
593
|
+
candidates.any? { |id|
|
594
|
+
s = id.to_s.strip.downcase
|
595
|
+
!s.empty? && previous.include?(s)
|
596
|
+
}
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
260
600
|
def identity_for_member(m)
|
261
601
|
if m.profile && !m.profile.strip.empty?
|
262
602
|
m.profile.strip.downcase
|
@@ -20,14 +20,32 @@ module Kettle
|
|
20
20
|
def run_cmd!(cmd)
|
21
21
|
# For Bundler-invoked build/release, explicitly prefix SKIP_GEM_SIGNING so
|
22
22
|
# the signing step is skipped even when Bundler scrubs ENV.
|
23
|
+
# Always do this on CI to avoid interactive prompts; locally only when explicitly requested.
|
23
24
|
if ENV["SKIP_GEM_SIGNING"] && cmd =~ /\Abundle(\s+exec)?\s+rake\s+(build|release)\b/
|
24
25
|
cmd = "SKIP_GEM_SIGNING=true #{cmd}"
|
25
26
|
end
|
26
27
|
puts "$ #{cmd}"
|
27
28
|
# Pass a plain Hash for the environment to satisfy tests and avoid ENV object oddities
|
28
29
|
env_hash = ENV.respond_to?(:to_hash) ? ENV.to_hash : ENV.to_h
|
29
|
-
|
30
|
-
|
30
|
+
|
31
|
+
# Capture output so we can surface clear diagnostics on failure
|
32
|
+
stdout_str, stderr_str, status = Open3.capture3(env_hash, cmd)
|
33
|
+
|
34
|
+
# Echo command output to match prior behavior
|
35
|
+
$stdout.print(stdout_str) unless stdout_str.nil? || stdout_str.empty?
|
36
|
+
$stderr.print(stderr_str) unless stderr_str.nil? || stderr_str.empty?
|
37
|
+
|
38
|
+
unless status.success?
|
39
|
+
exit_code = status.exitstatus
|
40
|
+
# Keep the original prefix to avoid breaking any tooling/tests that grep for it,
|
41
|
+
# but add the exit status and a brief diagnostic tail from stderr.
|
42
|
+
diag = ""
|
43
|
+
unless stderr_str.to_s.empty?
|
44
|
+
tail = stderr_str.lines.last(20).join
|
45
|
+
diag = "\n--- STDERR (last 20 lines) ---\n#{tail}".rstrip
|
46
|
+
end
|
47
|
+
abort("Command failed: #{cmd} (exit #{exit_code})#{diag}")
|
48
|
+
end
|
31
49
|
end
|
32
50
|
end
|
33
51
|
|
@@ -260,7 +260,7 @@ module Kettle
|
|
260
260
|
|
261
261
|
# We need to sleep a bit here to ensure the terminal is ready for both
|
262
262
|
# input and writing status updates to each workflow's line
|
263
|
-
sleep(0.2)
|
263
|
+
sleep(0.2)
|
264
264
|
|
265
265
|
selected = nil
|
266
266
|
# Create input thread always so specs that assert its cleanup/exception behavior can exercise it,
|
data/lib/kettle/dev/version.rb
CHANGED
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: 1.1.
|
4
|
+
version: 1.1.26
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter H. Boling
|
@@ -404,10 +404,10 @@ licenses:
|
|
404
404
|
- MIT
|
405
405
|
metadata:
|
406
406
|
homepage_uri: https://kettle-dev.galtzo.com/
|
407
|
-
source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.1.
|
408
|
-
changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.1.
|
407
|
+
source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.1.26
|
408
|
+
changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.1.26/CHANGELOG.md
|
409
409
|
bug_tracker_uri: https://github.com/kettle-rb/kettle-dev/issues
|
410
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.1.
|
410
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.1.26
|
411
411
|
funding_uri: https://github.com/sponsors/pboling
|
412
412
|
wiki_uri: https://github.com/kettle-rb/kettle-dev/wiki
|
413
413
|
news_uri: https://www.railsbling.com/tags/kettle-dev
|
metadata.gz.sig
CHANGED
@@ -1,3 +1,2 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
"�@+Ks�eM�AҾ�,��_e9��689�K&?�3�t�Q��m���Q�?*�F�6d���X����*p&��)�A�,�绒�$�
|
1
|
+
x�!���]�����G�4u��Q
|
2
|
+
��6�RM)J��_��-:�C���4Lz��
|