-
-
Save jpowell/981ba375e3059fa35e1aa6f694e9d521 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" | |
[[ -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