Last active
January 13, 2025 22:44
-
-
Save gburgett/17d00da4cbd018c52978f209cb3235e2 to your computer and use it in GitHub Desktop.
generates a changelog from git history, grabbing PRs from github
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /bin/bash | |
COLOR_NC='\033[0m' # No Color | |
COLOR_LIGHT_GREEN='\033[1;32m' | |
COLOR_GRAY='\033[1;30m' | |
COLOR_RED='\033[0;31m' | |
logv() { | |
[[ ! -z "$VERBOSE" ]] && >&2 echo -e "${COLOR_GRAY}$*${COLOR_NC}" | |
} | |
logerr() { | |
>&2 echo -e "${COLOR_RED}$*${COLOR_NC}" | |
exit -1 | |
} | |
usage() { | |
echo "$0 <from> <to> | |
Parse the git history to create a changelog from the latest release. | |
FROM: the initial commit from which to generate a changelog. | |
If 'all', then this will create a complete changelog with | |
changes grouped by 'Release*' tag. | |
If not given, the merge-base between HEAD and master is assumed. | |
TO: The end commit until which to generate a changelog. | |
If not given, 'HEAD' is assumed. | |
" && \ | |
grep " .)\ #" $0; exit 0; | |
} | |
while getopts ":hvm" arg; do | |
case $arg in | |
v) # Verbose mode - extra output | |
VERBOSE=true | |
FLAGS="$FLAGS -v" | |
;; | |
m) # Output in markdown format | |
MARKDOWN=true | |
;; | |
h | *) # Display help. | |
usage | |
exit 0 | |
;; | |
esac | |
done | |
shift $(($OPTIND - 1)) | |
[[ -z "$GITHUB_TOKEN" ]] && logerr "Please set Github API token in GITHUB_TOKEN environment variable" | |
command -v jq >/dev/null 2>&1 || logerr "I require 'jq' but it's not installed." | |
inside_git_repo="$(git rev-parse --is-inside-work-tree 2>/dev/null)" | |
[ ! "$inside_git_repo" ] && logerr "fatal: Not a git repository" | |
TO=$2 | |
[[ -z "$TO" ]] && TO=$(git rev-parse HEAD) | |
FROM=$1 | |
if [[ -z "$FROM" ]]; then | |
mains=("master" "main") | |
main="" | |
for m in "${mains[@]}"; do | |
if git rev-parse --quiet --verify "origin/$m" > /dev/null; then | |
main="$m" | |
fi | |
done | |
[[ -z "$main" ]] && logv "no main branch found" && return -1; | |
FROM=$(git merge-base "$TO" "origin/$main") | |
fi | |
if [[ "$FROM" == "all" ]]; then | |
TAGS=$(git tag --sort=-version:refname -l 'Release*') | |
LAST='' | |
while read -r line; do | |
$0 $FLAGS $line $LAST | |
[[ $? -ne 0 ]] && exit -1 | |
LAST=$line | |
echo "" | |
done <<< "${TAGS}" | |
INITIAL=$(git log --reverse --oneline | head -n 1 | awk '{ print $1 }') | |
$0 $FLAGS $INITIAL $LAST | |
exit $? | |
fi | |
FROM=$(git describe --exact-match --tags $FROM 2>/dev/null || echo "$FROM") | |
TO=$(git describe --exact-match --tags $TO 2>/dev/null || echo "$TO") | |
logv "git log ${FROM}..${TO}" | |
PRS=$(git log ${FROM}..${TO} --merges --oneline --grep '^Merge pull request') | |
logv "$PRS" | |
[[ -z "$PRS" ]] && exit 0 | |
PROJECT=$(git config --get remote.origin.url | sed -E 's/.*github\.com.(.*)(\.git)?/\1/') | |
PROJECT=${PROJECT%.git} | |
logv "Project is ${PROJECT}" | |
[[ "$PROJECT" == $(git config --get remote.origin.url) ]] && logerr "remote $PROJECT is not a github repo" | |
ISSUES_FIXED=() | |
echo "# Changelog From ${FROM} To ${TO}" | |
while read -r line; do | |
PR_NUM=$(echo $line | sed 's/.*\#\([0-9]*\).*/\1/') | |
logv "curl https://api.github.com/repos/${PROJECT}/pulls/${PR_NUM}" | |
PR_INFO=$(curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${PROJECT}/pulls/${PR_NUM} 2>/dev/null) | |
PR_TITLE=$(echo "${PR_INFO}" | jq -r '.title') | |
logv $PR_TITLE | |
if [[ $(echo "$PR_TITLE" | grep -i '^[[:space:]]*release') ]]; then | |
logv "Release PR - skipping" | |
continue | |
fi | |
PR_BODY=$(echo "${PR_INFO}" | jq -r '.body' | sed 's/\\n/\ | |
/g') | |
PR_USER=$(echo "${PR_INFO}" | jq -r '.user.login') | |
PR_MERGED_AT=$(echo "${PR_INFO}" | jq -r '.merged_at') | |
if [[ $(echo "$PR_USER" | grep -i '^dependabot') ]]; then | |
logv "Dependabot PR - skipping body" | |
PR_BODY='' | |
fi | |
PR_DATE=$(date -jf '%FT%TZ' "${PR_MERGED_AT}" +"%Y-%m-%d") | |
ISSUES_FIXED+=($(echo "$PR_BODY" | grep -i 'fixes\|closes\|refs #' | awk '{print $2}')) | |
echo "<details>" | |
echo "<summary>#${PR_NUM} ${PR_TITLE} @${PR_USER} merged ${PR_DATE}</summary>" | |
echo "" | |
echo "${PR_BODY}" | |
echo "" | |
echo "</details>" | |
done <<< "${PRS}" | |
scratch=$(mktemp) | |
function finish { | |
logv rm -rf "$scratch" | |
rm -rf "$scratch" | |
} | |
trap finish EXIT | |
for issue in "${ISSUES_FIXED[@]}" | |
do | |
logv "issue: ${issue}" | |
ISSUE_NUM=$(echo $issue | sed 's/.*\#\([0-9]*\).*/\1/') | |
logv "curl https://api.github.com/repos/${PROJECT}/issues/${ISSUE_NUM}" | |
ISSUE_INFO=$(curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${PROJECT}/issues/${ISSUE_NUM} 2>/dev/null) | |
[[ -z "$ISSUE_INFO" ]] && continue | |
RESP_MESSAGE=$(echo "$ISSUE_INFO" | jq -r .message) | |
[[ "$RESP_MESSAGE" == "Not Found" ]] && continue | |
logv $ISSUE_INFO | |
echo "$ISSUE_INFO" >> $scratch | |
done | |
BY_MILESTONE=$(jq --slurp -c 'group_by(.milestone.title) | .[]' $scratch) | |
echo "" | |
echo "## Issues closed in this release:" | |
while IFS= read -r milestone ; do | |
milestone_items=$(echo "$milestone" | jq -c '.[]') | |
CURRENT_MILESTONE= | |
while IFS= read -r ISSUE_INFO ; do | |
MILESTONE_URL=$(echo "${ISSUE_INFO}" | jq -r '.milestone.url') | |
if [[ "$CURRENT_MILESTONE" != "$MILESTONE_URL" ]]; then | |
logv "changing milestone to $MILESTONE_URL" | |
CURRENT_MILESTONE="$MILESTONE_URL" | |
if [[ "null" == "$MILESTONE_URL" ]]; then | |
echo "### No Milestone" | |
else | |
MILESTONE_TEXT=$(echo "${ISSUE_INFO}" | jq -r '.milestone.title') | |
MILESTONE_HTML_URL=$(echo "${ISSUE_INFO}" | jq -r '.milestone.html_url') | |
echo "### Milestone: [$MILESTONE_TEXT]($MILESTONE_HTML_URL)" | |
fi | |
fi | |
ISSUE_TITLE=$(echo "$ISSUE_INFO" | jq -r .title) | |
ISSUE_USER=$(echo "${ISSUE_INFO}" | jq -r '.user.login') | |
ISSUE_NUMBER=$(echo "$ISSUE_INFO" | jq -r .number) | |
echo "* #${ISSUE_NUMBER} ${ISSUE_TITLE}" | |
echo " - created by @${ISSUE_USER}" | |
done <<< "$milestone_items" | |
done <<< "$BY_MILESTONE" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment