|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# Gather the GitHub issues and pull requests that feed the release notes for a |
| 5 | +# given SDP release, straight from the stackabletech org. |
| 6 | +# |
| 7 | +# It searches for items carrying BOTH: |
| 8 | +# - a release label: release/<major.minor.patch> OR scheduled-for/<major.minor.patch> |
| 9 | +# - a release-note label: release-note OR release-note/action-required |
| 10 | +# |
| 11 | +# and prints a report grouped by repository, including the "Release notes" |
| 12 | +# snippet from each item body (the section the authors write in the PR template). |
| 13 | +# |
| 14 | +# It also flags two things that need human attention before a release: |
| 15 | +# - items still labelled `scheduled-for/<version>` (merged PRs should be |
| 16 | +# re-labelled `release/<version>`; open ones need to land or be dropped). |
| 17 | +# - open / unmerged PRs in the set. |
| 18 | +# |
| 19 | +# Counts are the ground truth from pagination, NOT the GitHub search |
| 20 | +# `total_count` (the web UI and that field can be misleading). |
| 21 | +# |
| 22 | +# Requires: gh (authenticated), jq. |
| 23 | +# |
| 24 | +# Usage: |
| 25 | +# scripts/gather-release-notes.sh -v <major.minor.patch> [--json] |
| 26 | +# |
| 27 | +# -v, --version Required. e.g. 26.7.0 |
| 28 | +# --json Dump the raw combined JSON (all items) instead of the report. |
| 29 | +# |
| 30 | +# Examples: |
| 31 | +# scripts/gather-release-notes.sh -v 26.7.0 |
| 32 | +# scripts/gather-release-notes.sh -v 26.7.0 --json > items.json |
| 33 | + |
| 34 | +# ------------------------------ |
| 35 | +# Args parsing |
| 36 | +# ------------------------------ |
| 37 | + |
| 38 | +VERSION="" |
| 39 | +OUTPUT="report" |
| 40 | + |
| 41 | +while [[ "$#" -gt 0 ]]; do |
| 42 | + case $1 in |
| 43 | + -v|--version) VERSION="$2"; shift ;; |
| 44 | + --json) OUTPUT="json" ;; |
| 45 | + -h|--help) sed -n '3,30p' "$0"; exit 0 ;; |
| 46 | + *) echo "Unknown parameter passed: $1" >&2; exit 1 ;; |
| 47 | + esac |
| 48 | + shift |
| 49 | +done |
| 50 | + |
| 51 | +if [ -z "$VERSION" ]; then |
| 52 | + echo "Usage: gather-release-notes.sh -v <major.minor.patch> [--json]" >&2 |
| 53 | + exit 1 |
| 54 | +fi |
| 55 | + |
| 56 | +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then |
| 57 | + echo "Invalid version format. Please use the major.minor.patch format (e.g. 26.7.0)." >&2 |
| 58 | + exit 1 |
| 59 | +fi |
| 60 | + |
| 61 | +for tool in gh jq; do |
| 62 | + if ! command -v "$tool" >/dev/null 2>&1; then |
| 63 | + echo "Required tool '$tool' not found on PATH." >&2 |
| 64 | + exit 1 |
| 65 | + fi |
| 66 | +done |
| 67 | + |
| 68 | +# Minor version, e.g. 26.7 from 26.7.0 |
| 69 | +MINOR=$(echo "$VERSION" | cut -d. -f1,2) |
| 70 | + |
| 71 | +RELEASE_LABEL="release/$VERSION" |
| 72 | +SCHEDULED_LABEL="scheduled-for/$VERSION" |
| 73 | +QUERY="org:stackabletech label:$SCHEDULED_LABEL,$RELEASE_LABEL label:release-note,release-note/action-required" |
| 74 | + |
| 75 | +# ------------------------------ |
| 76 | +# Fetch (single paginated search; the search API already returns bodies) |
| 77 | +# ------------------------------ |
| 78 | + |
| 79 | +ITEMS_FILE=$(mktemp) |
| 80 | +trap 'rm -f "$ITEMS_FILE"' EXIT |
| 81 | + |
| 82 | +# --paginate over search returns one JSON object per page; slurp the .items |
| 83 | +# arrays together and de-duplicate by html_url (defensive against overlap). |
| 84 | +gh api -X GET search/issues --paginate -f q="$QUERY" --jq '.items[]' \ |
| 85 | + | jq -s 'unique_by(.html_url)' > "$ITEMS_FILE" |
| 86 | + |
| 87 | +if [ "$OUTPUT" = "json" ]; then |
| 88 | + cat "$ITEMS_FILE" |
| 89 | + exit 0 |
| 90 | +fi |
| 91 | + |
| 92 | +# ------------------------------ |
| 93 | +# Helper: is this item an action-required one? |
| 94 | +# ------------------------------ |
| 95 | +is_action_required() { echo "$1" | jq -e 'any(.labels[].name; . == "release-note/action-required")' >/dev/null; } |
| 96 | + |
| 97 | +# ------------------------------ |
| 98 | +# Counts (ground truth = number of items actually returned) |
| 99 | +# ------------------------------ |
| 100 | +TOTAL=$(jq 'length' "$ITEMS_FILE") |
| 101 | +N_ISSUES=$(jq '[.[] | select(.pull_request == null)] | length' "$ITEMS_FILE") |
| 102 | +N_PRS=$(jq '[.[] | select(.pull_request != null)] | length' "$ITEMS_FILE") |
| 103 | + |
| 104 | +echo "# Release-notes source data for $VERSION (minor: $MINOR)" |
| 105 | +echo "#" |
| 106 | +echo "# Query: (label:$SCHEDULED_LABEL OR label:$RELEASE_LABEL)" |
| 107 | +echo "# AND (label:release-note OR label:release-note/action-required)" |
| 108 | +echo |
| 109 | +echo "## Counts (ground truth via pagination, not search total_count)" |
| 110 | +echo " Issues: $N_ISSUES" |
| 111 | +echo " PRs: $N_PRS" |
| 112 | +echo " Total: $TOTAL" |
| 113 | +echo |
| 114 | + |
| 115 | +# ------------------------------ |
| 116 | +# Hygiene flags |
| 117 | +# ------------------------------ |
| 118 | +echo "## Hygiene flags" |
| 119 | +echo |
| 120 | +echo "### Items still labelled '$SCHEDULED_LABEL'" |
| 121 | +echo "# (merged PRs should be re-labelled '$RELEASE_LABEL'; open ones must land or be dropped)" |
| 122 | +SCHEDULED=$(jq -r --arg L "$SCHEDULED_LABEL" ' |
| 123 | + [.[] | select(any(.labels[].name; . == $L))] |
| 124 | + | if length == 0 then " (none)" else |
| 125 | + .[] | " \(.repository_url | sub(".*/repos/stackabletech/";"")) #\(.number) [" + |
| 126 | + (if .pull_request == null then "issue" |
| 127 | + elif .pull_request.merged_at != null then "PR-MERGED" |
| 128 | + else "PR-OPEN" end) + "] \(.title)" |
| 129 | + end' "$ITEMS_FILE") |
| 130 | +echo "$SCHEDULED" |
| 131 | +echo |
| 132 | +echo "### Open / unmerged PRs in the set (resolve before release)" |
| 133 | +OPEN_PRS=$(jq -r ' |
| 134 | + [.[] | select(.pull_request != null and .state == "open")] |
| 135 | + | if length == 0 then " (none)" else |
| 136 | + .[] | " \(.repository_url | sub(".*/repos/stackabletech/";"")) #\(.number) \(.title)" |
| 137 | + end' "$ITEMS_FILE") |
| 138 | +echo "$OPEN_PRS" |
| 139 | +echo |
| 140 | + |
| 141 | +# ------------------------------ |
| 142 | +# Items grouped by repository, with release-note snippet |
| 143 | +# ------------------------------ |
| 144 | +echo "## Items grouped by repository" |
| 145 | + |
| 146 | +REPOS=$(jq -r '[.[] | .repository_url | sub(".*/repos/stackabletech/";"")] | unique | .[]' "$ITEMS_FILE") |
| 147 | + |
| 148 | +while IFS= read -r repo; do |
| 149 | + [ -z "$repo" ] && continue |
| 150 | + echo |
| 151 | + echo "### $repo" |
| 152 | + # Iterate items for this repo (compact JSON per line) |
| 153 | + jq -c --arg R "$repo" '.[] | select((.repository_url | sub(".*/repos/stackabletech/";"")) == $R)' "$ITEMS_FILE" \ |
| 154 | + | while IFS= read -r item; do |
| 155 | + number=$(echo "$item" | jq -r '.number') |
| 156 | + title=$(echo "$item" | jq -r '.title') |
| 157 | + url=$(echo "$item" | jq -r '.html_url') |
| 158 | + labels=$(echo "$item" | jq -r '[.labels[].name] | join(", ")') |
| 159 | + if echo "$item" | jq -e '.pull_request == null' >/dev/null; then |
| 160 | + kind="issue" |
| 161 | + elif echo "$item" | jq -e '.pull_request.merged_at != null' >/dev/null; then |
| 162 | + kind="PR-merged" |
| 163 | + else |
| 164 | + kind="PR-$(echo "$item" | jq -r '.state')" |
| 165 | + fi |
| 166 | + marker="" |
| 167 | + if is_action_required "$item"; then marker=" **ACTION-REQUIRED**"; fi |
| 168 | + |
| 169 | + echo "- #$number [$kind]$marker $title" |
| 170 | + echo " $url" |
| 171 | + echo " labels: $labels" |
| 172 | + # Extract the "Release note(s)" section from the body, if present. |
| 173 | + snippet=$(echo "$item" | jq -r '.body // ""' | awk ' |
| 174 | + BEGIN { grab=0 } |
| 175 | + /^#+[[:space:]]*[Rr]elease[[:space:]]*[Nn]ote/ { grab=1; next } |
| 176 | + grab && /^#+[[:space:]]/ { exit } |
| 177 | + grab { print } |
| 178 | + ' | sed 's/^/ /' | sed '/^[[:space:]]*$/d') |
| 179 | + if [ -n "$snippet" ]; then |
| 180 | + echo " release-note snippet:" |
| 181 | + echo "$snippet" |
| 182 | + else |
| 183 | + echo " release-note snippet: (none found in body)" |
| 184 | + fi |
| 185 | + done |
| 186 | +done <<< "$REPOS" |
0 commit comments