Skip to content

Instantly share code, notes, and snippets.

@jpowell
Forked from gburgett/changelog.sh
Last active January 14, 2025 19:16
Show Gist options
  • Save jpowell/981ba375e3059fa35e1aa6f694e9d521 to your computer and use it in GitHub Desktop.
Save jpowell/981ba375e3059fa35e1aa6f694e9d521 to your computer and use it in GitHub Desktop.
generates a changelog from git history, grabbing PRs from github
#! /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"
[[ -z "$LINEAR_TOKEN" ]] && logerr "Please set Linear API token in LINEAR_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
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 "Found PRs:"
logv "$PRS"
[[ -z "$PRS" ]] && logv "No PRs found in range" && 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"
echo "# Changelog From ${FROM} To ${TO}"
echo ""
echo "## Pull Requests"
echo ""
# Array to store all Linear issues
declare -a ALL_LINEAR_ISSUES
while read -r line; do
logv "Processing PR line: $line"
PR_NUM=$(echo $line | sed 's/.*\#\([0-9]*\).*/\1/')
logv "Extracted PR number: ${PR_NUM}"
logv "Fetching from 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)
logv "API Response: ${PR_INFO}"
# Add error handling for GitHub API response
if [[ -z "$PR_INFO" ]]; then
logv "Failed to fetch PR #${PR_NUM} info"
continue
fi
# Check if the API returned an error message
ERROR_MESSAGE=$(echo "$PR_INFO" | jq -r '.message')
if [[ "$ERROR_MESSAGE" != "null" ]]; then
logv "GitHub API error for PR #${PR_NUM}: $ERROR_MESSAGE"
continue
fi
PR_TITLE=$(echo "${PR_INFO}" | jq -r '.title')
if [[ "$PR_TITLE" == "null" || -z "$PR_TITLE" ]]; then
logv "Invalid PR title for #${PR_NUM}"
continue
fi
logv $PR_TITLE
if [[ $(echo "$PR_TITLE" | grep -i '^[[:space:]]*release') ]]; then
logv "Release PR - skipping"
continue
fi
PR_USER=$(echo "${PR_INFO}" | jq -r '.user.login')
if [[ "$PR_USER" == "null" || -z "$PR_USER" ]]; then
PR_USER="unknown"
fi
PR_MERGED_AT=$(echo "${PR_INFO}" | jq -r '.merged_at')
if [[ "$PR_MERGED_AT" == "null" || -z "$PR_MERGED_AT" ]]; then
PR_DATE="unknown"
else
if date --version >/dev/null 2>&1; then
# GNU date (Linux)
PR_DATE=$(date -d "${PR_MERGED_AT}" +"%Y-%m-%d")
else
# BSD date (macOS)
PR_DATE=$(date -jf '%FT%TZ' "${PR_MERGED_AT}" +"%Y-%m-%d")
fi
fi
echo "<details>"
echo "<summary>#${PR_NUM} ${PR_TITLE} (@${PR_USER} on ${PR_DATE})</summary>"
echo ""
# Add PR description if it exists
PR_BODY=$(echo "${PR_INFO}" | jq -r '.body')
if [[ "$PR_BODY" != "null" && ! -z "$PR_BODY" ]]; then
echo "$PR_BODY" | sed '/^[[:space:]]*$/d'
fi
echo "</details>"
echo ""
# Look for Linear issue references
logv "Checking PR body for Linear issues:"
logv "$PR_BODY"
CLOSE_LINES=$(echo "$PR_BODY" | grep -iE '[Cc]lose[s]? +OT-[0-9]+')
logv "Found close lines: $CLOSE_LINES"
if [[ ! -z "$CLOSE_LINES" ]]; then
LINEAR_ISSUES=$(echo "$CLOSE_LINES" | grep -oE 'OT-[0-9]+' | sed 's/OT-//')
logv "Extracted ticket numbers: $LINEAR_ISSUES"
while read -r issue_number; do
[[ ! -z "$issue_number" ]] && ALL_LINEAR_ISSUES+=("$issue_number")
done <<< "$LINEAR_ISSUES"
fi
done <<< "${PRS}"
# Now process all Linear issues
if [[ ${#ALL_LINEAR_ISSUES[@]} -gt 0 ]]; then
echo "## Closed Issues"
echo ""
# Remove duplicates from ALL_LINEAR_ISSUES
ALL_LINEAR_ISSUES=($(printf "%s\n" "${ALL_LINEAR_ISSUES[@]}" | sort -u))
for issue_number in "${ALL_LINEAR_ISSUES[@]}"; do
logv "Processing Linear issue: OT-${issue_number}"
QUERY='query($id: String!) { issue(id: $id) { id identifier title url description state { name } creator { displayName } } }'
VARIABLES="{\"id\": \"OT-${issue_number}\"}"
LINEAR_RESPONSE=$(curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: ${LINEAR_TOKEN}" \
--data "{\"query\": \"${QUERY}\", \"variables\": ${VARIABLES}}" \
https://api.linear.app/graphql)
logv "Linear API Response: ${LINEAR_RESPONSE}"
ERROR_MESSAGE=$(echo "$LINEAR_RESPONSE" | jq -r '.errors[0].message')
if [[ ! -z "$ERROR_MESSAGE" && "$ERROR_MESSAGE" != "null" ]]; then
logv "Linear API error: $ERROR_MESSAGE"
continue
fi
ISSUE_DATA=$(echo "$LINEAR_RESPONSE" | jq -r '.data.issue')
if [[ "$ISSUE_DATA" != "null" && ! -z "$ISSUE_DATA" ]]; then
ISSUE_ID=$(echo "$ISSUE_DATA" | jq -r '.identifier')
ISSUE_TITLE=$(echo "$ISSUE_DATA" | jq -r '.title')
ISSUE_URL=$(echo "$ISSUE_DATA" | jq -r '.url')
ISSUE_STATE=$(echo "$ISSUE_DATA" | jq -r '.state.name')
ISSUE_CREATOR=$(echo "$ISSUE_DATA" | jq -r '.creator.displayName')
echo "* [${ISSUE_ID}](${ISSUE_URL}) - ${ISSUE_TITLE}"
fi
done
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment