@cryptotaxi247 / kubo / commits / e0e6cacc4

bin/mkreleaselog: add github handle resolution and deduplication

- convert from zsh to bash for portability and shellcheck support - resolve GitHub handles via multiple methods: - noreply email pattern (user@users.noreply.github.com) - merge commit messages (Merge pull request #N from user/branch) - gh CLI API for PR authors (squash merge commits) - gh CLI API for commit authors (fallback for non-PR commits) - deduplicate contributors by GitHub handle instead of author name - cache resolved mappings in ~/.cache/mkreleaselog/github-handles.json - output clickable GitHub profile links in contributor table

Marcin Rataj committed Nov 27, 2025 at 02:06 UTC e0e6cacc49d61a2f9f80bf3f51040c4fb9a74b4a
1 file changed +459 -68
bin/mkreleaselog
+459 -68
@@ -1,10 +1,19 @@
1 -#!/bin/zsh
1 +#!/bin/bash
2 #
3 # Invocation: mkreleaselog [FIRST_REF [LAST_REF]]
4 +#
5 +# Generates release notes with contributor statistics, deduplicating by GitHub handle.
6 +# GitHub handles are resolved from:
7 +# 1. GitHub noreply emails (user@users.noreply.github.com)
8 +# 2. Merge commit messages (Merge pull request #N from user/branch)
9 +# 3. GitHub API via gh CLI (for squash merges)
10 +#
11 +# Results are cached in ~/.cache/mkreleaselog/github-handles.json
12
13 set -euo pipefail
14 export GO111MODULE=on
7 -export GOPATH="$(go env GOPATH)"
15 +GOPATH="$(go env GOPATH)"
16 +export GOPATH
17
18 # List of PCRE regular expressions to match "included" modules.
19 INCLUDE_MODULES=(
@@ -47,16 +56,349 @@ IGNORE_FILES=(
56 "*.gen.go"
57 )
58
59 +##########################################################################################
60 +# GitHub Handle Resolution Infrastructure
61 +##########################################################################################
62 +
63 +# Cache location following XDG spec
64 +GITHUB_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/mkreleaselog"
65 +GITHUB_CACHE_FILE="$GITHUB_CACHE_DIR/github-handles.json"
66 +
67 +# Timeout for gh CLI commands (seconds)
68 +GH_TIMEOUT=10
69 +
70 +# Associative array for email -> github handle mapping (runtime cache)
71 +declare -A EMAIL_TO_GITHUB
72 +
73 +# Check if gh CLI is available and authenticated
74 +gh_available() {
75 + command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1
76 +}
77 +
78 +# Load cached email -> github handle mappings from disk
79 +load_github_cache() {
80 + EMAIL_TO_GITHUB=()
81 +
82 + if [[ ! -f "$GITHUB_CACHE_FILE" ]]; then
83 + return 0
84 + fi
85 +
86 + # Validate JSON before loading
87 + if ! jq -e '.' "$GITHUB_CACHE_FILE" >/dev/null 2>&1; then
88 + msg "Warning: corrupted cache file, ignoring"
89 + return 0
90 + fi
91 +
92 + local email handle
93 + while IFS=$'\t' read -r email handle; do
94 + # Validate handle format (alphanumeric, hyphens, max 39 chars)
95 + if [[ -n "$email" && -n "$handle" && "$handle" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$ ]]; then
96 + EMAIL_TO_GITHUB["$email"]="$handle"
97 + fi
98 + done < <(jq -r 'to_entries[] | "\(.key)\t\(.value)"' "$GITHUB_CACHE_FILE" 2>/dev/null)
99 +
100 + msg "Loaded ${#EMAIL_TO_GITHUB[@]} cached GitHub handle mappings"
101 +}
102 +
103 +# Save email -> github handle mappings to disk (atomic write)
104 +save_github_cache() {
105 + if [[ ${#EMAIL_TO_GITHUB[@]} -eq 0 ]]; then
106 + return 0
107 + fi
108 +
109 + mkdir -p "$GITHUB_CACHE_DIR"
110 +
111 + local tmp_file
112 + tmp_file="$(mktemp "$GITHUB_CACHE_DIR/cache.XXXXXX")" || return 1
113 +
114 + # Build JSON from associative array
115 + {
116 + echo "{"
117 + local first=true
118 + local key
119 + for key in "${!EMAIL_TO_GITHUB[@]}"; do
120 + if [[ "$first" == "true" ]]; then
121 + first=false
122 + else
123 + echo ","
124 + fi
125 + # Escape special characters in email for JSON
126 + printf ' %s: %s' "$(jq -n --arg e "$key" '$e')" "$(jq -n --arg h "${EMAIL_TO_GITHUB[$key]}" '$h')"
127 + done
128 + echo
129 + echo "}"
130 + } > "$tmp_file"
131 +
132 + # Validate before replacing
133 + if jq -e '.' "$tmp_file" >/dev/null 2>&1; then
134 + mv "$tmp_file" "$GITHUB_CACHE_FILE"
135 + msg "Saved ${#EMAIL_TO_GITHUB[@]} GitHub handle mappings to cache"
136 + else
137 + rm -f "$tmp_file"
138 + msg "Warning: failed to save cache (invalid JSON)"
139 + fi
140 +}
141 +
142 +# Extract GitHub handle from email if it's a GitHub noreply address
143 +# Handles: user@users.noreply.github.com and 12345678+user@users.noreply.github.com
144 +extract_handle_from_noreply() {
145 + local email="$1"
146 +
147 + if [[ "$email" =~ ^([0-9]+\+)?([a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?)@users\.noreply\.github\.com$ ]]; then
148 + echo "${BASH_REMATCH[2]}"
149 + return 0
150 + fi
151 + return 1
152 +}
153 +
154 +# Extract GitHub handle from merge commit subject
155 +# Handles: "Merge pull request #123 from username/branch"
156 +extract_handle_from_merge_commit() {
157 + local subject="$1"
158 +
159 + if [[ "$subject" =~ ^Merge\ pull\ request\ \#[0-9]+\ from\ ([a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?)/.*$ ]]; then
160 + echo "${BASH_REMATCH[1]}"
161 + return 0
162 + fi
163 + return 1
164 +}
165 +
166 +# Extract PR number from commit subject
167 +# Handles: "Subject (#123)" and "Merge pull request #123 from"
168 +extract_pr_number() {
169 + local subject="$1"
170 +
171 + if [[ "$subject" =~ \(#([0-9]+)\)$ ]]; then
172 + echo "${BASH_REMATCH[1]}"
173 + return 0
174 + elif [[ "$subject" =~ ^Merge\ pull\ request\ \#([0-9]+)\ from ]]; then
175 + echo "${BASH_REMATCH[1]}"
176 + return 0
177 + fi
178 + return 1
179 +}
180 +
181 +# Query GitHub API for PR author (with timeout and error handling)
182 +query_pr_author() {
183 + local gh_repo="$1" # e.g., "ipfs/kubo"
184 + local pr_num="$2"
185 +
186 + if ! gh_available; then
187 + return 1
188 + fi
189 +
190 + local handle
191 + handle="$(timeout "$GH_TIMEOUT" gh pr view "$pr_num" --repo "$gh_repo" --json author -q '.author.login' 2>/dev/null)" || return 1
192 +
193 + # Validate handle format
194 + if [[ -n "$handle" && "$handle" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$ ]]; then
195 + echo "$handle"
196 + return 0
197 + fi
198 + return 1
199 +}
200 +
201 +# Query GitHub API for commit author (fallback when no PR available)
202 +query_commit_author() {
203 + local gh_repo="$1" # e.g., "ipfs/kubo"
204 + local commit_sha="$2"
205 +
206 + if ! gh_available; then
207 + return 1
208 + fi
209 +
210 + local handle
211 + handle="$(timeout "$GH_TIMEOUT" gh api "/repos/$gh_repo/commits/$commit_sha" --jq '.author.login // empty' 2>/dev/null)" || return 1
212 +
213 + # Validate handle format
214 + if [[ -n "$handle" && "$handle" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$ ]]; then
215 + echo "$handle"
216 + return 0
217 + fi
218 + return 1
219 +}
220 +
221 +# Resolve email to GitHub handle using all available methods
222 +# Args: email, commit_hash (optional), repo_dir (optional), gh_repo (optional)
223 +resolve_github_handle() {
224 + local email="$1"
225 + local commit="${2:-}"
226 + local repo_dir="${3:-}"
227 + local gh_repo="${4:-}"
228 +
229 + # Skip empty emails
230 + [[ -z "$email" ]] && return 1
231 +
232 + # Check runtime cache first
233 + if [[ -n "${EMAIL_TO_GITHUB[$email]:-}" ]]; then
234 + echo "${EMAIL_TO_GITHUB[$email]}"
235 + return 0
236 + fi
237 +
238 + local handle=""
239 +
240 + # Method 1: Extract from noreply email
241 + if handle="$(extract_handle_from_noreply "$email")"; then
242 + EMAIL_TO_GITHUB["$email"]="$handle"
243 + echo "$handle"
244 + return 0
245 + fi
246 +
247 + # Method 2: Look at commit message for merge commit pattern
248 + if [[ -n "$commit" && -n "$repo_dir" ]]; then
249 + local subject
250 + subject="$(git -C "$repo_dir" log -1 --format='%s' "$commit" 2>/dev/null)" || true
251 +
252 + if [[ -n "$subject" ]]; then
253 + if handle="$(extract_handle_from_merge_commit "$subject")"; then
254 + EMAIL_TO_GITHUB["$email"]="$handle"
255 + echo "$handle"
256 + return 0
257 + fi
258 +
259 + # Method 3: Query GitHub API for PR author
260 + if [[ -n "$gh_repo" ]]; then
261 + local pr_num
262 + if pr_num="$(extract_pr_number "$subject")"; then
263 + if handle="$(query_pr_author "$gh_repo" "$pr_num")"; then
264 + EMAIL_TO_GITHUB["$email"]="$handle"
265 + echo "$handle"
266 + return 0
267 + fi
268 + fi
269 + fi
270 + fi
271 + fi
272 +
273 + return 1
274 +}
275 +
276 +# Build GitHub handle mappings for all commits in a range
277 +# This does a single pass to collect PR numbers, then batch queries them
278 +build_github_mappings() {
279 + local module="$1"
280 + local start="$2"
281 + local end="${3:-HEAD}"
282 + local repo
283 + repo="$(strip_version "$module")"
284 + local dir
285 + local gh_repo=""
286 +
287 + if [[ "$module" == "github.com/ipfs/kubo" ]]; then
288 + dir="$ROOT_DIR"
289 + else
290 + dir="$GOPATH/src/$repo"
291 + fi
292 +
293 + # Extract gh_repo for API calls (e.g., "ipfs/kubo" from "github.com/ipfs/kubo")
294 + if [[ "$repo" =~ ^github\.com/(.+)$ ]]; then
295 + gh_repo="${BASH_REMATCH[1]}"
296 + fi
297 +
298 + msg "Building GitHub handle mappings for $module..."
299 +
300 + # Collect all unique emails and their commit context
301 + declare -A email_commits=()
302 + local hash email subject
303 +
304 + while IFS=$'\t' read -r hash email subject; do
305 + [[ -z "$email" ]] && continue
306 +
307 + # Skip if already resolved
308 + [[ -n "${EMAIL_TO_GITHUB[$email]:-}" ]] && continue
309 +
310 + # Try to resolve without API first
311 + local handle=""
312 +
313 + # Method 1: noreply email
314 + if handle="$(extract_handle_from_noreply "$email")"; then
315 + EMAIL_TO_GITHUB["$email"]="$handle"
316 + continue
317 + fi
318 +
319 + # Method 2: merge commit message
320 + if handle="$(extract_handle_from_merge_commit "$subject")"; then
321 + EMAIL_TO_GITHUB["$email"]="$handle"
322 + continue
323 + fi
324 +
325 + # Store for potential API lookup
326 + if [[ -z "${email_commits[$email]:-}" ]]; then
327 + email_commits["$email"]="$hash"
328 + fi
329 + done < <(git -C "$dir" log --format='tformat:%H%x09%aE%x09%s' --no-merges "$start..$end" 2>/dev/null)
330 +
331 + # API batch lookup for remaining emails (if gh is available)
332 + if gh_available && [[ -n "$gh_repo" && ${#email_commits[@]} -gt 0 ]]; then
333 + msg "Querying GitHub API for ${#email_commits[@]} unknown contributors..."
334 + local key
335 + for key in "${!email_commits[@]}"; do
336 + # Skip if already resolved
337 + [[ -n "${EMAIL_TO_GITHUB[$key]:-}" ]] && continue
338 +
339 + local commit_hash="${email_commits[$key]}"
340 + local subj handle
341 + subj="$(git -C "$dir" log -1 --format='%s' "$commit_hash" 2>/dev/null)" || true
342 +
343 + # Try PR author lookup first (cheaper API call)
344 + local pr_num
345 + if pr_num="$(extract_pr_number "$subj")"; then
346 + if handle="$(query_pr_author "$gh_repo" "$pr_num")"; then
347 + EMAIL_TO_GITHUB["$key"]="$handle"
348 + continue
349 + fi
350 + fi
351 +
352 + # Fallback: commit author API (works for any commit)
353 + if handle="$(query_commit_author "$gh_repo" "$commit_hash")"; then
354 + EMAIL_TO_GITHUB["$key"]="$handle"
355 + fi
356 + done
357 + fi
358 +}
359 +
360 +##########################################################################################
361 +# Original infrastructure with modifications
362 ##########################################################################################
363
364 +build_include_regex() {
365 + local result=""
366 + local mod
367 + for mod in "${INCLUDE_MODULES[@]}"; do
368 + if [[ -n "$result" ]]; then
369 + result="$result|$mod"
370 + else
371 + result="$mod"
372 + fi
373 + done
374 + echo "($result)"
375 +}
376 +
377 +build_exclude_regex() {
378 + local result=""
379 + local mod
380 + for mod in "${EXCLUDE_MODULES[@]}"; do
381 + if [[ -n "$result" ]]; then
382 + result="$result|$mod"
383 + else
384 + result="$mod"
385 + fi
386 + done
387 + if [[ -n "$result" ]]; then
388 + echo "($result)"
389 + else
390 + echo '$^' # match nothing
391 + fi
392 +}
393 +
394 if [[ ${#INCLUDE_MODULES[@]} -gt 0 ]]; then
53 - INCLUDE_REGEX="(${$(printf "|%s" "${INCLUDE_MODULES[@]}"):1})"
395 + INCLUDE_REGEX="$(build_include_regex)"
396 else
397 INCLUDE_REGEX="" # "match anything"
398 fi
399
400 if [[ ${#EXCLUDE_MODULES[@]} -gt 0 ]]; then
59 - EXCLUDE_REGEX="(${$(printf "|%s" "${EXCLUDE_MODULES[@]}"):1})"
401 + EXCLUDE_REGEX="$(build_exclude_regex)"
402 else
403 EXCLUDE_REGEX='$^' # "match nothing"
404 fi
@@ -71,8 +413,6 @@ NL=$'\n'
413
414 ROOT_DIR="$(git rev-parse --show-toplevel)"
415
74 -alias jq="jq --unbuffered"
75 -
416 msg() {
417 echo "$*" >&2
418 }
@@ -80,11 +420,21 @@ msg() {
420 statlog() {
421 local module="$1"
422 local rpath
423 + local gh_repo=""
424 +
425 if [[ "$module" == "github.com/ipfs/kubo" ]]; then
426 rpath="$ROOT_DIR"
427 else
428 rpath="$GOPATH/src/$(strip_version "$module")"
429 fi
430 +
431 + # Extract gh_repo for API calls
432 + local repo
433 + repo="$(strip_version "$module")"
434 + if [[ "$repo" =~ ^github\.com/(.+)$ ]]; then
435 + gh_repo="${BASH_REMATCH[1]}"
436 + fi
437 +
438 local start="${2:-}"
439 local end="${3:-HEAD}"
440 local mailmap_file="$rpath/.mailmap"
@@ -93,18 +443,21 @@ statlog() {
443 fi
444
445 local stack=()
96 - git -C "$rpath" -c mailmap.file="$mailmap_file" log --use-mailmap --shortstat --no-merges --pretty="tformat:%H%x09%aN%x09%aE" "$start..$end" -- . "${IGNORE_FILES_PATHSPEC[@]}" | while read -r line; do
446 + local line
447 + while read -r line; do
448 if [[ -n "$line" ]]; then
449 stack+=("$line")
450 continue
451 fi
452
453 + local changes
454 read -r changes
455
104 - changed=0
105 - insertions=0
106 - deletions=0
107 - while read count event; do
456 + local changed=0
457 + local insertions=0
458 + local deletions=0
459 + local count event
460 + while read -r count event; do
461 if [[ "$event" =~ ^file ]]; then
462 changed=$count
463 elif [[ "$event" =~ ^insertion ]]; then
@@ -117,27 +470,32 @@ statlog() {
470 fi
471 done<<<"${changes//,/$NL}"
472
473 + local author
474 for author in "${stack[@]}"; do
475 + local hash name email
476 IFS=$'\t' read -r hash name email <<<"$author"
477 +
478 + # Resolve GitHub handle
479 + local github_handle=""
480 + github_handle="$(resolve_github_handle "$email" "$hash" "$rpath" "$gh_repo")" || true
481 +
482 jq -n \
483 --arg "hash" "$hash" \
484 --arg "name" "$name" \
485 --arg "email" "$email" \
486 + --arg "github" "$github_handle" \
487 --argjson "changed" "$changed" \
488 --argjson "insertions" "$insertions" \
489 --argjson "deletions" "$deletions" \
129 - '{Commit: $hash, Author: $name, Email: $email, Files: $changed, Insertions: $insertions, Deletions: $deletions}'
490 + '{Commit: $hash, Author: $name, Email: $email, GitHub: $github, Files: $changed, Insertions: $insertions, Deletions: $deletions}'
491 done
492 stack=()
132 - done
493 + done < <(git -C "$rpath" -c mailmap.file="$mailmap_file" log --use-mailmap --shortstat --no-merges --pretty="tformat:%H%x09%aN%x09%aE" "$start..$end" -- . "${IGNORE_FILES_PATHSPEC[@]}")
494 }
495
496 # Returns a stream of deps changed between $1 and $2.
497 dep_changes() {
137 - {
138 - <"$1"
139 - <"$2"
140 - } | jq -s 'JOIN(INDEX(.[0][]; .Path); .[1][]; .Path; {Path: .[0].Path, Old: (.[1] | del(.Path)), New: (.[0] | del(.Path))}) | select(.New.Version != .Old.Version)'
498 + cat "$1" "$2" | jq -s 'JOIN(INDEX(.[0][]; .Path); .[1][]; .Path; {Path: .[0].Path, Old: (.[1] | del(.Path)), New: (.[0] | del(.Path))}) | select(.New.Version != .Old.Version)'
499 }
500
501 # resolve_commits resolves a git ref for each version.
@@ -165,12 +523,11 @@ ignored_commit() {
523
524 # Generate a release log for a range of commits in a single repo.
525 release_log() {
168 - setopt local_options BASH_REMATCH
169 -
526 local module="$1"
527 local start="$2"
528 local end="${3:-HEAD}"
173 - local repo="$(strip_version "$1")"
529 + local repo
530 + repo="$(strip_version "$1")"
531 local dir
532 if [[ "$module" == "github.com/ipfs/kubo" ]]; then
533 dir="$ROOT_DIR"
@@ -178,28 +535,25 @@ release_log() {
535 dir="$GOPATH/src/$repo"
536 fi
537
181 - local commit pr
182 - git -C "$dir" log \
183 - --format='tformat:%H %s' \
184 - --first-parent \
185 - "$start..$end" |
186 - while read commit subject; do
187 - # Skip commits that only touch ignored files.
188 - if ignored_commit "$dir" "$commit"; then
189 - continue
190 - fi
538 + local commit subject
539 + while read -r commit subject; do
540 + # Skip commits that only touch ignored files.
541 + if ignored_commit "$dir" "$commit"; then
542 + continue
543 + fi
544
192 - if [[ "$subject" =~ '^Merge pull request #([0-9]+) from' ]]; then
193 - local prnum="${BASH_REMATCH[2]}"
194 - local desc="$(git -C "$dir" show --summary --format='tformat:%b' "$commit" | head -1)"
195 - printf -- "- %s (%s)\n" "$desc" "$(pr_link "$repo" "$prnum")"
196 - elif [[ "$subject" =~ '\(#([0-9]+)\)$' ]]; then
197 - local prnum="${BASH_REMATCH[2]}"
198 - printf -- "- %s (%s)\n" "$subject" "$(pr_link "$repo" "$prnum")"
199 - else
200 - printf -- "- %s\n" "$subject"
201 - fi
202 - done
545 + if [[ "$subject" =~ ^Merge\ pull\ request\ \#([0-9]+)\ from ]]; then
546 + local prnum="${BASH_REMATCH[1]}"
547 + local desc
548 + desc="$(git -C "$dir" show --summary --format='tformat:%b' "$commit" | head -1)"
549 + printf -- "- %s (%s)\n" "$desc" "$(pr_link "$repo" "$prnum")"
550 + elif [[ "$subject" =~ \(#([0-9]+)\)$ ]]; then
551 + local prnum="${BASH_REMATCH[1]}"
552 + printf -- "- %s (%s)\n" "$subject" "$(pr_link "$repo" "$prnum")"
553 + else
554 + printf -- "- %s\n" "$subject"
555 + fi
556 + done < <(git -C "$dir" log --format='tformat:%H %s' --first-parent "$start..$end")
557 }
558
559 indent() {
@@ -211,7 +565,8 @@ mod_deps() {
565 }
566
567 ensure() {
214 - local repo="$(strip_version "$1")"
568 + local repo
569 + repo="$(strip_version "$1")"
570 local commit="$2"
571 local rpath
572 if [[ "$1" == "github.com/ipfs/kubo" ]]; then
@@ -232,14 +587,27 @@ ensure() {
587 git -C "$rpath" rev-parse --verify "$commit" >/dev/null || return 1
588 }
589
590 +# Summarize stats, grouping by GitHub handle (with fallback to email for dedup)
591 statsummary() {
236 - jq -s 'group_by(.Author)[] | {Author: .[0].Author, Commits: (. | length), Insertions: (map(.Insertions) | add), Deletions: (map(.Deletions) | add), Files: (map(.Files) | add)}' |
237 - jq '. + {Lines: (.Deletions + .Insertions)}'
592 + jq -s '
593 + # Group by GitHub handle if available, otherwise by email
594 + group_by(if .GitHub != "" then .GitHub else .Email end)[] |
595 + {
596 + # Use first non-empty GitHub handle, or fall back to Author name
597 + Author: .[0].Author,
598 + GitHub: (map(select(.GitHub != "")) | .[0].GitHub // ""),
599 + Email: .[0].Email,
600 + Commits: (. | length),
601 + Insertions: (map(.Insertions) | add),
602 + Deletions: (map(.Deletions) | add),
603 + Files: (map(.Files) | add)
604 + }
605 + ' | jq '. + {Lines: (.Deletions + .Insertions)}'
606 }
607
608 strip_version() {
609 local repo="$1"
242 - if [[ "$repo" =~ '.*/v[0-9]+$' ]]; then
610 + if [[ "$repo" =~ .*/v[0-9]+$ ]]; then
611 repo="$(dirname "$repo")"
612 fi
613 echo "$repo"
@@ -248,16 +616,24 @@ strip_version() {
616 recursive_release_log() {
617 local start="${1:-$(git tag -l | sort -V | grep -v -- '-rc' | grep 'v'| tail -n1)}"
618 local end="${2:-$(git rev-parse HEAD)}"
251 - local repo_root="$(git rev-parse --show-toplevel)"
252 - local module="$(go list -m)"
253 - local dir="$(go list -m -f '{{.Dir}}')"
619 + local repo_root
620 + repo_root="$(git rev-parse --show-toplevel)"
621 + local module
622 + module="$(go list -m)"
623 + local dir
624 + dir="$(go list -m -f '{{.Dir}}')"
625 +
626 + # Load cached GitHub handle mappings
627 + load_github_cache
628
629 # Kubo can be run from any directory, dependencies still use GOPATH
630
631 (
632 local result=0
259 - local workspace="$(mktemp -d)"
260 - trap "$(printf 'rm -rf "%q"' "$workspace")" INT TERM EXIT
633 + local workspace
634 + workspace="$(mktemp -d)"
635 + # shellcheck disable=SC2064
636 + trap "rm -rf '$workspace'" INT TERM EXIT
637 cd "$workspace"
638
639 echo "Computing old deps..." >&2
@@ -272,6 +648,9 @@ recursive_release_log() {
648
649 printf -- "Generating Changelog for %s %s..%s\n" "$module" "$start" "$end" >&2
650
651 + # Pre-build GitHub mappings for main module
652 + build_github_mappings "$module" "$start" "$end"
653 +
654 echo "### 📝 Changelog"
655 echo
656 echo "<details><summary>Full Changelog</summary>"
@@ -282,24 +661,26 @@ recursive_release_log() {
661
662 statlog "$module" "$start" "$end" > statlog.json
663
285 - dep_changes old_deps.json new_deps.json |
664 + local dep_module new new_ref old old_ref
665 + while read -r dep_module new new_ref old old_ref; do
666 + if ! ensure "$dep_module" "$new_ref"; then
667 + result=1
668 + local changelog="failed to fetch repo"
669 + else
670 + # Pre-build GitHub mappings for dependency
671 + build_github_mappings "$dep_module" "$old_ref" "$new_ref"
672 + statlog "$dep_module" "$old_ref" "$new_ref" >> statlog.json
673 + local changelog
674 + changelog="$(release_log "$dep_module" "$old_ref" "$new_ref")"
675 + fi
676 + if [[ -n "$changelog" ]]; then
677 + printf -- "- %s (%s -> %s):\n" "$dep_module" "$old" "$new"
678 + echo "$changelog" | indent
679 + fi
680 + done < <(dep_changes old_deps.json new_deps.json |
681 jq --arg inc "$INCLUDE_REGEX" --arg exc "$EXCLUDE_REGEX" \
682 'select(.Path | test($inc)) | select(.Path | test($exc) | not)' |
288 - # Compute changelogs
289 - jq -r '"\(.Path) \(.New.Version) \(.New.Ref) \(.Old.Version) \(.Old.Ref // "")"' |
290 - while read module new new_ref old old_ref; do
291 - if ! ensure "$module" "$new_ref"; then
292 - result=1
293 - local changelog="failed to fetch repo"
294 - else
295 - statlog "$module" "$old_ref" "$new_ref" >> statlog.json
296 - local changelog="$(release_log "$module" "$old_ref" "$new_ref")"
297 - fi
298 - if [[ -n "$changelog" ]]; then
299 - printf -- "- %s (%s -> %s):\n" "$module" "$old" "$new"
300 - echo "$changelog" | indent
301 - fi
302 - done
683 + jq -r '"\(.Path) \(.New.Version) \(.New.Ref) \(.Old.Version) \(.Old.Ref // "")"')
684
685 echo
686 echo "</details>"
@@ -311,8 +692,18 @@ recursive_release_log() {
692 echo "|-------------|---------|---------|---------------|"
693 statsummary <statlog.json |
694 jq -s 'sort_by(.Lines) | reverse | .[]' |
314 - jq -r '"| \(.Author) | \(.Commits) | +\(.Insertions)/-\(.Deletions) | \(.Files) |"'
315 - return "$status"
695 + jq -r '
696 + if .GitHub != "" then
697 + "| [@\(.GitHub)](https://github.com/\(.GitHub)) | \(.Commits) | +\(.Insertions)/-\(.Deletions) | \(.Files) |"
698 + else
699 + "| \(.Author) | \(.Commits) | +\(.Insertions)/-\(.Deletions) | \(.Files) |"
700 + end
701 + '
702 +
703 + # Save cache before exiting
704 + save_github_cache
705 +
706 + return "$result"
707 )
708 }
709