Class: Kettle::Dev::ReadmeBackers
- Inherits:
-
Object
- Object
- Kettle::Dev::ReadmeBackers
- Defined in:
- lib/kettle/dev/readme_backers.rb
Defined Under Namespace
Classes: Backer
Constant Summary collapse
- README_PATH =
Default README is the one in the current working directory of the host project
File.("README.md", Dir.pwd)
- README_OSC_TAG_DEFAULT =
"OPENCOLLECTIVE"
- COMMIT_SUBJECT_DEFAULT =
"💸 Thanks 🙏 to our new backers 🎒 and subscribers 📜"
- OC_YML_PATH =
Deprecated constant maintained for backwards compatibility in tests/specs.
Prefer OpenCollectiveConfig.yaml_path going forward, but resolve to the host project root. OpenCollectiveConfig.yaml_path(Dir.pwd)
Instance Method Summary collapse
-
#initialize(handle: nil, readme_path: README_PATH) ⇒ ReadmeBackers
constructor
A new instance of ReadmeBackers.
-
#run! ⇒ Object
-
#validate ⇒ void
Validate environment preconditions for running the updater.
Constructor Details
#initialize(handle: nil, readme_path: README_PATH) ⇒ ReadmeBackers
Returns a new instance of ReadmeBackers.
62 63 64 65 |
# File 'lib/kettle/dev/readme_backers.rb', line 62 def initialize(handle: nil, readme_path: README_PATH) @handle = handle || resolve_handle @readme_path = readme_path end |
Instance Method Details
#run! ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/kettle/dev/readme_backers.rb', line 91 def run! validate debug_log("Starting run: handle=#{@handle.inspect}, readme=#{@readme_path}") debug_log("Resolved OSC tag base=#{readme_osc_tag.inspect}") readme = File.read(@readme_path) # Identify previous entries for diffing/mentions b_start, b_end = (readme) s_start_prev, s_end_prev = (readme) 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}") prev_backer_identities = extract_section_identities(readme, b_start, b_end) prev_sponsor_identities = extract_section_identities(readme, s_start_prev, s_end_prev) # Fetch all BACKER-role members once and partition by tier debug_log("Fetching OpenCollective members JSON for handle=#{@handle} ...") raw = fetch_all_backers_raw debug_log("Fetched #{Array(raw).size} members (role=#{Backer::ROLE}) before tier partitioning") # Build OpenCollective type-index map to generate stable avatar/website links index_map = build_oc_index_map(raw) if Kettle::Dev::DEBUGGING tier_counts = Array(raw).group_by { |h| (h["tier"] || "").to_s.strip }.transform_values(&:size) debug_log("Tier distribution: #{tier_counts}") empty_tier = Array(raw).select { |h| h["tier"].to_s.strip.empty? } unless empty_tier.empty? debug_log("Members with empty tier: count=#{empty_tier.size}; showing up to 5 samples:") empty_tier.first(5).each_with_index do |m, i| debug_log(" [empty-tier ##{i + 1}] name=#{m["name"].inspect}, isActive=#{m["isActive"].inspect}, profile=#{m["profile"].inspect}, website=#{m["website"].inspect}") end end other_tiers = Array(raw).map { |h| h["tier"].to_s.strip }.reject { |t| t.empty? || t.casecmp("Backer").zero? || t.casecmp("Sponsor").zero? } unless other_tiers.empty? counts = other_tiers.group_by { |t| t }.transform_values(&:size) debug_log("Non-standard tiers present (excluding Backer/Sponsor): #{counts}") end end backers_hashes = Array(raw).select { |h| h["tier"].to_s.strip.casecmp("Backer").zero? } sponsors_hashes = Array(raw).select { |h| h["tier"].to_s.strip.casecmp("Sponsor").zero? } backers = map_hashes_to_backers(backers_hashes, index_map, force_type: "backer") sponsors = map_hashes_to_backers(sponsors_hashes, index_map, force_type: "organization") debug_log("Partitioned counts => Backers=#{backers.size}, Sponsors=#{sponsors.size}") if Kettle::Dev::DEBUGGING && backers.empty? && sponsors.empty? && Array(raw).any? 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.") end # Additional dynamic tiers (exclude Backer/Sponsor) extra_map = {} Array(raw).group_by { |h| h["tier"].to_s.strip }.each do |tier, members| normalized = tier.empty? ? "Donors" : tier next if normalized.casecmp("Backer").zero? || normalized.casecmp("Sponsor").zero? extra_map[normalized] = map_hashes_to_backers(members, index_map) end debug_log("Extra tiers detected: #{extra_map.keys.sort}") unless extra_map.empty? backers_md = generate_markdown(backers, empty_message: "No backers yet. Be the first!", default_name: "Backer") sponsors_md_base = generate_markdown(sponsors, empty_message: "No sponsors yet. Be the first!", default_name: "Sponsor") extra_tiers_md = generate_extra_tiers_markdown(extra_map) sponsors_md = if extra_tiers_md.empty? sponsors_md_base else [sponsors_md_base, "", extra_tiers_md].join("\n") end # Update backers section # If the identities in the existing block match the identities derived from current data, # treat as no-change to avoid rewriting formatting (e.g., Markdown -> HTML OC anchors). semantically_same_backers = semantically_same_section?(prev_backer_identities, backers) updated = if semantically_same_backers :no_change else (readme, b_start, b_end, backers_md) end case updated when :not_found debug_log("Backers tag block not found; skipping backers section update") updated_readme = readme backers_changed = false new_backers = [] when :no_change debug_log("Backers section unchanged (identities match or generated markdown matches existing block)") updated_readme = readme backers_changed = false new_backers = [] else updated_readme = updated backers_changed = true new_backers = compute_new_members(prev_backer_identities, backers) debug_log("Backers section updated; new_backers=#{new_backers.size}") end # Update sponsors section (with extra tiers appended when present) s_start, s_end = (updated_readme) # If there is no sponsors section but there is a backers section, append extra tiers to backers instead. if s_start == :not_found && !extra_tiers_md.empty? && b_start != :not_found debug_log("Sponsors tags not found; appending extra tiers under Backers section") backers_md_with_extra = [backers_md, "", extra_tiers_md].join("\n") updated = (updated_readme, b_start, b_end, backers_md_with_extra) updated_readme = updated unless updated == :no_change || updated == :not_found end # Sponsors: apply the same semantic no-change rule prev_s_ids = extract_section_identities(updated_readme, s_start, s_end) semantically_same_sponsors = semantically_same_section?(prev_s_ids, sponsors) updated2 = if semantically_same_sponsors :no_change else (updated_readme, s_start, s_end, sponsors_md) end case updated2 when :not_found debug_log("Sponsors tag block not found; skipping sponsors section update") sponsors_changed = false final = updated_readme new_sponsors = [] when :no_change debug_log("Sponsors section unchanged (identities match or generated markdown matches existing block)") sponsors_changed = false final = updated_readme new_sponsors = [] else sponsors_changed = true final = updated2 new_sponsors = compute_new_members(prev_sponsor_identities, sponsors) debug_log("Sponsors section updated; new_sponsors=#{new_sponsors.size}") end if !backers_changed && !sponsors_changed if b_start == :not_found && s_start == :not_found ts = tag_strings warn("No recognized Open Collective tags found in #{@readme_path}. Expected one or more of: " \ "#{ts[:generic_start]}/#{ts[:generic_end]}, #{ts[:individuals_start]}/#{ts[:individuals_end]}, #{ts[:orgs_start]}/#{ts[:orgs_end]}.") debug_log("Missing tags: looked for #{ts}") # Do not exit the process during tests or library use; just return. return end debug_log("No changes detected after processing; Backers=#{backers.size}, Sponsors=#{sponsors.size}, ExtraTiers=#{extra_map.keys.size}") puts "No changes to backers or sponsors sections in #{@readme_path}." return end File.write(@readme_path, final) msgs = [] msgs << "backers" if backers_changed msgs << "sponsors" if sponsors_changed puts "Updated #{msgs.join(" and ")} section#{{true => "s", false => ""}[msgs.size > 1]} in #{@readme_path}." # Compose and perform commit with mentions if in a git repo perform_git_commit(new_backers, new_sponsors) if git_repo? && (backers_changed || sponsors_changed) end |
#validate ⇒ void
This method returns an undefined value.
Validate environment preconditions for running the updater.
Ensures README_UPDATER_TOKEN is present. If missing, prints guidance and raises.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/kettle/dev/readme_backers.rb', line 72 def validate token = ENV["README_UPDATER_TOKEN"].to_s if token.strip.empty? repo = ENV["REPO"] || ENV["GITHUB_REPOSITORY"] org = repo&.to_s&.split("/")&.first org_url = if org && !org.strip.empty? "https://github.com/organizations/#{org}/settings/secrets/actions" else "https://github.com/organizations/YOUR_ORG/settings/secrets/actions" end $stderr.puts "ERROR: README_UPDATER_TOKEN is not set." $stderr.puts "Please create an organization-level Actions secret named README_UPDATER_TOKEN at:" $stderr.puts " #{org_url}" $stderr.puts "Then update the workflow to reference it, or provide README_UPDATER_TOKEN in the environment." raise 'Missing ENV["README_UPDATER_TOKEN"]' end nil end |