Class: Kettle::Dev::ChangelogCLI
- Inherits:
-
Object
- Object
- Kettle::Dev::ChangelogCLI
- Defined in:
- lib/kettle/dev/changelog_cli.rb,
sig/kettle/dev/changelog_cli.rbs
Overview
CLI for updating CHANGELOG.md with new version sections
Constant Summary collapse
- UNRELEASED_SECTION_HEADING =
"[Unreleased]:"- CHANGELOG_VERSION_PATTERN =
/\d+\.\d+\.\d+(?:[.-][0-9A-Za-z]+)*/- CHANGELOG_VERSION_PATTERN_SOURCE =
CHANGELOG_VERSION_PATTERN.source
- LINK_REF_DEF_RE =
Matches a Markdown link-reference definition line, e.g.
[key]: https://... /^\s*\[[^\]]+\]:\s+\S+/- DEEP_HEADING_RE =
Matches an ATX heading at H4 or deeper (####, #####, ...)
/^\#{4,}\s/
Instance Method Summary collapse
-
#abort(msg) ⇒ void
Abort with error message.
-
#convert_heading_tag_suffix_to_list(text) ⇒ String
Convert legacy heading tag suffix to list format.
-
#coverage_lines ⇒ [String?, String?]
Get coverage lines from coverage.json.
-
#detect_initial_compare_base(_lines = nil) ⇒ String
Detect initial compare base from changelog.
-
#detect_previous_version(after_text) ⇒ String?
Detect previous version from after text.
-
#detect_version ⇒ String
Detect version from lib/**/version.rb.
-
#ensure_footer_spacing(text) ⇒ String
Ensure proper footer spacing.
-
#extract_unreleased(content) ⇒ [String?, String?, String?]
Extract unreleased section from changelog.
-
#filter_unreleased_sections(unreleased_block) ⇒ String
Filter unreleased sections keeping only those with content.
-
#initialize(strict: true, enforce_coverage_thresholds: true, update_prep: false, version: nil, root: Kettle::Dev::CIHelpers.project_root) ⇒ ChangelogCLI
constructor
Initialize the changelog CLI Sets up paths for CHANGELOG.md and coverage.json.
-
#normalize_heading_spacing(text) ⇒ String
Normalize spacing around headings.
- #pending_release_status ⇒ Hash[Symbol, untyped]
- #release_state ⇒ Hash[Symbol, untyped]
- #release_state_table(state = release_state) ⇒ String
-
#run ⇒ void
Main entry point that updates CHANGELOG.md.
-
#update_link_refs(content, owner, repo, prev_version, new_version) ⇒ Object
Update link references in changelog.
-
#yard_percent_documented ⇒ String?
Get YARD documentation percentage.
Constructor Details
#initialize(strict: true, enforce_coverage_thresholds: true, update_prep: false, version: nil, root: Kettle::Dev::CIHelpers.project_root) ⇒ ChangelogCLI
Initialize the changelog CLI Sets up paths for CHANGELOG.md and coverage.json
30 31 32 33 34 35 36 37 38 |
# File 'lib/kettle/dev/changelog_cli.rb', line 30 def initialize(strict: true, enforce_coverage_thresholds: true, update_prep: false, version: nil, root: Kettle::Dev::CIHelpers.project_root) @root = root @changelog_path = File.join(@root, "CHANGELOG.md") @coverage_path = File.join(@root, "coverage", "coverage.json") @strict = strict @enforce_coverage_thresholds = enforce_coverage_thresholds @update_prep = update_prep @version_override = Kettle::Dev::Versioning.normalize_explicit_version(version) end |
Instance Method Details
#abort(msg) ⇒ void
This method returns an undefined value.
Abort with error message
27 28 29 |
# File 'sig/kettle/dev/changelog_cli.rbs', line 27 def abort(msg) Kettle::Dev::ExitAdapter.abort(msg) end |
#convert_heading_tag_suffix_to_list(text) ⇒ String
Convert legacy heading tag suffix to list format
800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 |
# File 'lib/kettle/dev/changelog_cli.rb', line 800 def convert_heading_tag_suffix_to_list(text) lines = text.lines # Build a set of versions that have a tag reference (e.g., "[1.2.3t]: ..."). # IMPORTANT: Only scan the footer link-ref block (starting at the [Unreleased]: line) # to avoid accidentally picking up body content. scan_start = lines.index { |l| l.start_with?(UNRELEASED_SECTION_HEADING) } || lines.length t_versions = {} non_t_tag_refs = {} lines[scan_start..-1].to_a.each do |l| # Case A: explicit tag ref key like [1.2.3t]: ... if (m = l.match(/^\[(#{CHANGELOG_VERSION_PATTERN_SOURCE})t\]:\s+(\S+)/o)) t_versions[m[1]] = true next end # Case B: non-t ref that nevertheless points to a tag URL (GitHub or GitLab) if (m2 = l.match(/^\[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\]:\s+(\S+)/o)) url = m2[2] # Accept only when the URL clearly points to a tag for the SAME version # Support both GitHub and GitLab style tag URLs if (murl = url.match(%r{/(?:releases/)?tags?/v(#{CHANGELOG_VERSION_PATTERN_SOURCE})}io)) version_in_url = murl[1] if version_in_url == m2[1] non_t_tag_refs[m2[1]] = url end end end end # Any version that has either explicit t-ref or a non-t tag-ref is considered tagged tag_ref_versions = {} t_versions.keys.each { |v| tag_ref_versions[v] = true } non_t_tag_refs.keys.each { |v| tag_ref_versions[v] = true } out = [] i = 0 while i < lines.length line = lines[i] # Case 1: Heading contains legacy tag suffix we should convert m = line.match(/^## \[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\](.*)\(\[tag\]\[(#{CHANGELOG_VERSION_PATTERN_SOURCE})t\]\)\s*$/io) if m && m[1] == m[3] ver = m[1] middle = m[2] new_heading = ("## [#{ver}]" + middle).rstrip + "\n" out << new_heading # If the next non-blank line is already a TAG list item, don't add another k = i + 1 k += 1 while k < lines.length && lines[k].strip == "" unless k < lines.length && lines[k].lstrip.start_with?("- TAG:") out << "\n" out << "- TAG: [v#{ver}][#{ver}t]\n" out << "\n" end # Skip any existing blank lines following the heading to avoid duplicate spacing i = k next end # Case 2: Heading does NOT contain suffix, but a matching tag ref exists; ensure a TAG list item if (m2 = line.match(/^## \[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\](.*)$/o)) ver2 = m2[1] # Skip Unreleased heading and non-release headings unless ver2.nil? k = i + 1 k += 1 while k < lines.length && lines[k].strip == "" needs_tag = tag_ref_versions[ver2] && !(k < lines.length && lines[k].lstrip.start_with?("- TAG:")) if needs_tag out << (line.end_with?("\n") ? line : line + "\n") out << "\n" out << "- TAG: [v#{ver2}][#{ver2}t]\n" out << "\n" i = k next end end end # Footer duplication: if we are in the footer block and encounter a non-t tag-ref # without a matching t-ref, emit the t-ref immediately after with the same URL. if i >= scan_start if (mref = line.match(/^\[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\]:\s+(\S+)/o)) vref = mref[1] mref[2] if non_t_tag_refs[vref] && !t_versions[vref] out << line out << "[#{vref}t]: #{non_t_tag_refs[vref]}\n" t_versions[vref] = true i += 1 next end end end out << line i += 1 end out.join end |
#coverage_lines ⇒ [String?, String?]
Get coverage lines from coverage.json
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 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 |
# File 'sig/kettle/dev/changelog_cli.rbs', line 42 def coverage_lines if @strict # Always generate fresh coverage data in strict mode # Delete old coverage files to ensure we get current data coverage_dir = File.dirname(@coverage_path) if Dir.exist?(coverage_dir) puts "Cleaning old coverage data from #{Kettle::Dev.display_path(coverage_dir)}..." Dir.glob(File.join(coverage_dir, "*")).each do |file| File.delete(file) if File.file?(file) end end puts "Generating fresh coverage data by running: bundle exec kettle-test" success = system(changelog_coverage_env, "bundle", "exec", "kettle-test", chdir: @root) unless success raise "bundle exec kettle-test failed with exit status #{$?.exitstatus || "unknown"}" end puts "Coverage generation complete." ensure_changelog_coverage_json! else # Non-strict mode: check if coverage.json exists, warn if not unless File.file?(@coverage_path) warn() warn("Run: K_SOUP_COV_FORMATTERS=json bundle exec kettle-test to generate it") return [nil, nil] end end # Parse the coverage data data = JSON.parse(File.read(@coverage_path)) files = data["coverage"] || {} file_count = 0 total_lines = 0 covered_lines = 0 total_branches = 0 covered_branches = 0 files.each_value do |h| lines = h["lines"] || [] line_relevant = lines.count { |x| x.is_a?(Integer) } line_covered = lines.count { |x| x.is_a?(Integer) && x > 0 } if line_relevant > 0 file_count += 1 total_lines += line_relevant covered_lines += line_covered end branches = h["branches"] || [] branches.each do |b| next unless b.is_a?(Hash) cov = b["coverage"] next unless cov.is_a?(Numeric) total_branches += 1 covered_branches += 1 if cov > 0 end end line_pct = (total_lines > 0) ? ((covered_lines.to_f / total_lines) * 100.0) : 0.0 branch_pct = (total_branches > 0) ? ((covered_branches.to_f / total_branches) * 100.0) : 0.0 line_str = format("COVERAGE: %.2f%% -- %d/%d lines in %d files", line_pct, covered_lines, total_lines, file_count) branch_str = format("BRANCH COVERAGE: %.2f%% -- %d/%d branches in %d files", branch_pct, covered_branches, total_branches, file_count) [line_str, branch_str] rescue JSON::ParserError => e if @strict raise "Failed to parse coverage JSON at #{@coverage_path}: #{e.class}: #{e.}" else warn("Failed to parse coverage: #{e.class}: #{e.}") [nil, nil] end rescue => e if @strict raise "Failed to get coverage data: #{e.class}: #{e.}" else warn("Failed to get coverage data: #{e.class}: #{e.}") [nil, nil] end end |
#detect_initial_compare_base(_lines = nil) ⇒ String
Detect initial compare base from changelog
1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 |
# File 'lib/kettle/dev/changelog_cli.rb', line 1063 def detect_initial_compare_base(_lines = nil) env_sha = ENV.fetch("KETTLE_CHANGELOG_INITIAL_SHA", nil) return env_sha.strip if env_sha && !env_sha.strip.empty? sha = git_root_commit return sha if sha warn( "Could not determine initial git root commit; using HEAD^ as compare base. " \ "Set KETTLE_CHANGELOG_INITIAL_SHA to override." ) "HEAD^" end |
#detect_previous_version(after_text) ⇒ String?
Detect previous version from after text
36 37 38 39 40 41 42 |
# File 'sig/kettle/dev/changelog_cli.rbs', line 36 def detect_previous_version(after_text) # after_text begins with the first released section following Unreleased m = after_text.match(/^## \[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\]/o) return m[1] if m nil end |
#detect_version ⇒ String
Detect version from lib/**/version.rb
30 31 32 |
# File 'sig/kettle/dev/changelog_cli.rbs', line 30 def detect_version Kettle::Dev::Versioning.detect_version(@root, override: @version_override) end |
#ensure_footer_spacing(text) ⇒ String
Ensure proper footer spacing
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'sig/kettle/dev/changelog_cli.rbs', line 63 def (text) lines = text.split("\n", -1) # Find the Unreleased link-ref which denotes start of footer refs idx = lines.index { |l| l.start_with?(UNRELEASED_SECTION_HEADING) } return text unless idx head = lines[0...idx] tail = lines[idx..-1] # Ensure exactly one blank line between body and refs if head.any? && head.last.to_s.strip != "" head << "" elsif head.any? && head.last.to_s.strip == "" && head[-2].to_s.strip == "" # Collapse multiple blanks before footer to a single head.pop while head.any? && head.last.to_s.strip == "" head << "" end (head + tail).join("\n") end |
#extract_unreleased(content) ⇒ [String?, String?, String?]
Extract unreleased section from changelog
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'sig/kettle/dev/changelog_cli.rbs', line 33 def extract_unreleased(content) lines = content.lines start_i = lines.index { |l| l.start_with?("## [Unreleased]") } return [nil, nil, nil] unless start_i # Find the next version heading after Unreleased next_i = (start_i + 1) while next_i < lines.length && !lines[next_i].start_with?("## [") next_i += 1 end # Now next_i points to the next section heading or EOF before = lines[0..(start_i - 1)].join unreleased_body = lines[(start_i + 1)..(next_i - 1)] || [] after_lines = lines[next_i..-1] || [] # When this is the very first release there is no `## [X.Y.Z]` heading to act # as a boundary, so the footer link-ref block ([Unreleased]: ...) sits at the # end of the unreleased body. Move everything from the [Unreleased]: line # onward into `after` so those refs are not mistaken for section content. if next_i == lines.length = unreleased_body.index { |l| l.start_with?(UNRELEASED_SECTION_HEADING) } if after_lines = unreleased_body[..-1] + after_lines unreleased_body = unreleased_body[0...] end end unreleased_block = unreleased_body.join after = after_lines.join [unreleased_block, before, after] end |
#filter_unreleased_sections(unreleased_block) ⇒ String
Filter unreleased sections keeping only those with content
575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 |
# File 'lib/kettle/dev/changelog_cli.rb', line 575 def filter_unreleased_sections(unreleased_block) lines = unreleased_block.lines out = [] i = 0 while i < lines.length line = lines[i] if line.start_with?("### ") header = line i += 1 chunk = [] while i < lines.length && !lines[i].start_with?("### ") && !lines[i].start_with?("## ") chunk << lines[i] i += 1 end # A section has real content only if it contains at least one non-blank line that is # neither a link-reference definition ([key]: url) nor a deeper heading (H4+). # Link-ref defs and H4+ headings alone are not meaningful section content. content_present = chunk.any? { |l| l.strip != "" && l !~ LINK_REF_DEF_RE && l !~ DEEP_HEADING_RE } if content_present # Trim leading blank lines so there is no blank line after the header while chunk.any? && chunk.first.strip == "" chunk.shift end # Trim trailing blank lines while chunk.any? && chunk.last.strip == "" chunk.pop end out << header out.concat(chunk) out << "\n" unless out.last&.end_with?("\n") end next else # Lines outside sections are ignored for released sections i += 1 end end out.join end |
#normalize_heading_spacing(text) ⇒ String
Normalize spacing around headings
998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 |
# File 'lib/kettle/dev/changelog_cli.rb', line 998 def normalize_heading_spacing(text) lines = text.split("\n", -1) out = [] in_fence = false fence_re = /^\s*```/ heading_re = /^\s*#+\s+.+/ lines.each_with_index do |ln, idx| if fence_re.match?(ln) in_fence = !in_fence out << ln next end if !in_fence && heading_re.match?(ln) # Ensure previous line is blank (unless start of file or already blank) prev_blank = out.empty? ? false : out.last.to_s.strip == "" out << "" unless out.empty? || prev_blank out << ln # Peek at next line in source to decide if we need to inject a blank now. nxt = lines[idx + 1] out << "" unless nxt.to_s.strip == "" else out << ln end end # Collapse multiple consecutive blank lines down to a single between regions that our logic might have doubled collapsed = [] lines_enum = out lines_enum.each do |l| if l.strip == "" && collapsed.last.to_s.strip == "" next end collapsed << l end collapsed.join("\n") end |
#pending_release_status ⇒ Hash[Symbol, untyped]
142 143 144 |
# File 'lib/kettle/dev/changelog_cli.rb', line 142 def pending_release_status release_state end |
#release_state ⇒ Hash[Symbol, untyped]
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 |
# File 'lib/kettle/dev/changelog_cli.rb', line 146 def release_state changelog_present = ensure_changelog_for_release_state! version = detect_version gem_name = detect_gem_name unless changelog_present latest_overall, latest_for_series, latest_for_major = latest_released_versions(gem_name, version) latest_target = latest_release_target(version, latest_overall, latest_for_series, latest_for_major) return { root: @root, gem_name: gem_name, version: version, changelog_present: false, pending: false, pending_release: false, unreleased_entries: false, prepared_release_pending: false, latest_changelog_version: nil, latest_released: latest_target || latest_overall, latest_released_overall: latest_overall, latest_released_for_current_major: latest_for_major, latest_released_for_current_series: latest_for_series, latest_release_target: latest_target } end changelog = File.read(@changelog_path) unreleased_block, _before, after = extract_unreleased(changelog) unreleased_entries = unreleased_block_has_entries?(unreleased_block) latest_changelog_version = detect_previous_version(after.to_s) release_lookup_version = latest_changelog_version || version latest_overall, latest_for_series, latest_for_major = latest_released_versions(gem_name, release_lookup_version) latest_target = latest_release_target(release_lookup_version, latest_overall, latest_for_series, latest_for_major) prepared_release_pending = !!latest_changelog_version && latest_target != latest_changelog_version { root: @root, gem_name: gem_name, version: version, changelog_present: true, pending: unreleased_entries || prepared_release_pending, pending_release: unreleased_entries || prepared_release_pending, unreleased_entries: unreleased_entries, prepared_release_pending: prepared_release_pending, latest_changelog_version: latest_changelog_version, latest_released: latest_target || latest_overall, latest_released_overall: latest_overall, latest_released_for_current_major: latest_for_major, latest_released_for_current_series: latest_for_series, latest_release_target: latest_target } end |
#release_state_table(state = release_state) ⇒ String
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/kettle/dev/changelog_cli.rb', line 198 def release_state_table(state = release_state) rows = [ ["gem", "version.rb", "latest released", "latest changelog", "unreleased", "prepared", "pending"], [ state.fetch(:gem_name), state.fetch(:version), state.fetch(:latest_released) || "unknown", state.fetch(:latest_changelog_version) || "none", yes_no(state.fetch(:unreleased_entries)), yes_no(state.fetch(:prepared_release_pending)), yes_no(state.fetch(:pending_release)) ] ] widths = rows.transpose.map { |column| column.map(&:length).max } rows.map.with_index do |row, index| line = row.each_with_index.map { |value, i| value.ljust(widths.fetch(i)) }.join(" ").rstrip if index == 0 [line, widths.map { |width| "-" * width }.join(" ")].join("\n") else line end end.join("\n") end |
#run ⇒ void
This method returns an undefined value.
Main entry point that updates CHANGELOG.md
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 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 |
# File 'lib/kettle/dev/changelog_cli.rb', line 46 def run version = detect_version today = Time.now.strftime("%Y-%m-%d") owner, repo = Kettle::Dev::CIHelpers.repo_info unless owner && repo warn("Could not determine GitHub owner/repo from origin remote.") warn("Make sure 'origin' points to github.com. Alternatively, set origin or update links manually afterward.") end changelog = File.read(@changelog_path) plan = @update_prep ? explicit_update_prep_plan(changelog) : detect_plan(changelog, version) confirm_plan!(plan) if plan.fetch(:action) == :reformat_only reformat_changelog!(changelog) return end line_cov_line, branch_cov_line = coverage_lines yard_line = yard_percent_documented if plan.fetch(:action) == :update_prepared_release update_prepared_release!(changelog, today, owner, repo, line_cov_line, branch_cov_line, yard_line) return end unreleased_block, before, after = extract_unreleased(changelog) if unreleased_block.nil? abort("Could not find '## [Unreleased]' section in CHANGELOG.md") end if unreleased_block.strip.empty? warn("No entries found under Unreleased. Creating an empty version section anyway.") end prev_version = detect_previous_version(after) new_section = +"" new_section << "## [#{version}] - #{today}\n" new_section << "- TAG: [v#{version}][#{version}t]\n" new_section << "- #{line_cov_line}\n" if line_cov_line new_section << "- #{branch_cov_line}\n" if branch_cov_line new_section << "- #{yard_line}\n" if yard_line new_section << filter_unreleased_sections(unreleased_block) # Ensure exactly one blank line separates this new section from the next section new_section.rstrip! new_section << "\n\n" # Reset the Unreleased section to empty category headings unreleased_reset = <<~MD ## [Unreleased] ### Added ### Changed ### Deprecated ### Removed ### Fixed ### Security MD # Preserve everything from the first released section down to the line containing the [Unreleased] link ref. # Many real-world changelogs intersperse stray link refs between sections; we should keep them. updated = before + unreleased_reset + "\n" + new_section # Find the [Unreleased]: link-ref line and append everything from the start of the first released section # through to the end of the file, but if a [Unreleased]: ref exists, ensure we do not duplicate the # section content above it. if after && !after.empty? # Split 'after' by lines so we can locate the first link-ref to Unreleased after_lines = after.lines unreleased_ref_idx = after_lines.index { |l| l.start_with?(UNRELEASED_SECTION_HEADING) } if unreleased_ref_idx # Keep all content prior to the link-ref (older releases and interspersed refs) preserved_body = after_lines[0...unreleased_ref_idx].join # Then append the tail starting from the Unreleased link-ref line to preserve the footer refs = after_lines[unreleased_ref_idx..-1].join updated << preserved_body << else # No Unreleased ref found; just append the remainder as-is updated << after end end updated = update_link_refs(updated, owner, repo, prev_version, version) # Transform legacy heading suffix tags into list items under headings updated = convert_heading_tag_suffix_to_list(updated) # Normalize spacing around headings to aid Markdown renderers updated = normalize_heading_spacing(updated) # Ensure exactly one trailing newline at EOF updated = updated.rstrip + "\n" File.write(@changelog_path, updated) puts "CHANGELOG.md updated with v#{version} section." end |
#update_link_refs(content, owner, repo, prev_version, new_version) ⇒ Object
Update link references in changelog
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 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 |
# File 'sig/kettle/dev/changelog_cli.rbs', line 51 def update_link_refs(content, owner, repo, prev_version, new_version) # Convert any GitLab links to GitHub content = content.gsub(%r{https://gitlab\.com/([^/]+)/([^/]+)/-/compare/([^.]+)\.\.\.([^\s]+)}) do o = owner || Regexp.last_match(1) r = repo || Regexp.last_match(2) from = Regexp.last_match(3) to = Regexp.last_match(4) "https://github.com/#{o}/#{r}/compare/#{from}...#{to}" end content = content.gsub(%r{https://gitlab\.com/([^/]+)/([^/]+)/-/tags/(v[^\s\]]+)}) do o = owner || Regexp.last_match(1) r = repo || Regexp.last_match(2) tag = Regexp.last_match(3) "https://github.com/#{o}/#{r}/releases/tag/#{tag}" end # Append or update the bottom reference links lines = content.lines # Identify the true start of the footer reference block: the line with the [Unreleased] link-ref. # Do NOT assume the first link-ref after the Unreleased heading starts the footer, because # some changelogs contain interspersed link-refs within section bodies. unreleased_ref_idx = lines.index { |l| l.start_with?(UNRELEASED_SECTION_HEADING) } # If no [Unreleased]: ref is present, consider the reference block to start at EOF first_ref = unreleased_ref_idx || lines.length # Ensure Unreleased points to GitHub compare from new tag to HEAD if owner && repo unreleased_ref = "[Unreleased]: https://github.com/#{owner}/#{repo}/compare/v#{new_version}...HEAD\n" # Update an existing Unreleased ref only if it appears after Unreleased heading; otherwise append idx = nil lines.each_with_index do |l, i| if l.start_with?(UNRELEASED_SECTION_HEADING) && i >= first_ref idx = i break end end if idx lines[idx] = unreleased_ref else lines << unreleased_ref end end if owner && repo # Add compare link for the new version from = prev_version ? "v#{prev_version}" : detect_initial_compare_base(lines) new_compare = "[#{new_version}]: https://github.com/#{owner}/#{repo}/compare/#{from}...v#{new_version}\n" unless lines.any? { |l| l.start_with?("[#{new_version}]:") } lines << new_compare end # Add tag link for the new version new_tag = "[#{new_version}t]: https://github.com/#{owner}/#{repo}/releases/tag/v#{new_version}\n" unless lines.any? { |l| l.start_with?("[#{new_version}t]:") } lines << new_tag end end # Rebuild and sort the reference block so Unreleased is first, then newest to oldest versions, preserving everything above first_ref ref_lines = lines[first_ref..-1].select { |l| /^\[[^\]]+\]:\s+http/.match?(l) } # Deduplicate by key (text inside the square brackets) by_key = {} ref_lines.each do |l| if l =~ /^\[([^\]]+)\]:\s+/ by_key[$1] = l end end unreleased_line = by_key.delete("Unreleased") # Separate version compare and tag links compares = {} = {} by_key.each do |k, v| if k =~ /^(#{CHANGELOG_VERSION_PATTERN_SOURCE})$/o compares[$1] = v elsif k =~ /^(#{CHANGELOG_VERSION_PATTERN_SOURCE})t$/o [$1] = v end end # Build a unified set of versions that appear in either compares or tags version_keys = (compares.keys | .keys) # Sort versions descending (newest to oldest) sorted_versions = version_keys.map { |s| Gem::Version.new(s) }.sort.reverse.map(&:to_s) new_ref_block = [] new_ref_block << unreleased_line if unreleased_line sorted_versions.each do |v| new_ref_block << compares[v] if compares[v] new_ref_block << [v] if [v] end # Replace the old block head = lines[0...first_ref] # Ensure exactly one blank line separating body content from the reference block if head.any? && head.last.to_s.strip != "" head << "\n" end rebuilt = head + new_ref_block + ["\n"] rebuilt.join end |
#yard_percent_documented ⇒ String?
Get YARD documentation percentage
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'sig/kettle/dev/changelog_cli.rbs', line 45 def yard_percent_documented commands = yard_documentation_commands if commands.empty? if @strict raise "bin/rake and bin/yard not found or not executable; ensure rake and yard are installed via bundler" else warn("bin/rake and bin/yard not found or not executable; ensure rake and yard are installed via bundler") return end end begin # Run the canonical docs task to get the documentation percentage. out = +"" commands.each do |command| prepare_yard_fence_tmp_files if command == [File.join(@root, "bin", "yard")] output, _status = Open3.capture2(*command, {chdir: @root}) out << output line = documented_percent_line(output) return line if line end if @strict raise "Could not find documented percentage in bin/rake yard output" else warn("Could not find documented percentage in bin/rake yard output.") nil end rescue => e if @strict raise "Failed to run bin/rake yard: #{e.class}: #{e.}" else warn("Failed to run bin/rake yard: #{e.class}: #{e.}") nil end end end |