Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions .github/workflows/generate-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
name: Generate Changelog

on:
workflow_call:
outputs:
changelog:
description: "The generated changelog content"
value: ${{ jobs.generate.outputs.changelog }}

jobs:
generate:
name: Generate Changelog
runs-on: ubuntu-latest
outputs:
changelog: ${{ steps.changelog.outputs.CHANGELOG }}

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for changelog generation

- name: Generate changelog
id: changelog
run: |
# Get previous release tag
# NOTE: This must run BEFORE creating the new tag, otherwise git describe
# will find the new tag and the changelog will be empty
PREVIOUS_TAG=$(git describe --abbrev=0 --tags --match "v*" 2>/dev/null || echo "")

echo "Previous tag: ${PREVIOUS_TAG:-'(none - first release)'}"

# Get commit SHAs since last release
if [ -z "$PREVIOUS_TAG" ]; then
COMMIT_SHAS=$(git log --pretty=format:"%H" --no-merges)
else
COMMIT_SHAS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%H" --no-merges)
fi

echo "Found $(echo "$COMMIT_SHAS" | wc -l) commits since last release"

# Collect all closed issues by tracing commits -> PRs -> issues
declare -A CLOSED_ISSUES # issue_number -> issue_title
declare -A HIGHLIGHTS # issue_number -> highlight message

for SHA in $COMMIT_SHAS; do
echo "Processing commit: $SHA"

# Find PR associated with this commit (squash merge)
PR_DATA=$(gh pr list --search "$SHA" --state merged --json number,body --limit 1 2>/dev/null || echo "[]")

if [ "$PR_DATA" != "[]" ] && [ -n "$PR_DATA" ]; then
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty')
PR_BODY=$(echo "$PR_DATA" | jq -r '.[0].body // ""')

if [ -n "$PR_NUMBER" ]; then
echo " Found PR #$PR_NUMBER"

# Extract issue numbers from PR body (Fixes #XX, Closes #XX, Resolves #XX)
ISSUE_NUMBERS=$(echo "$PR_BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "")

for ISSUE_NUM in $ISSUE_NUMBERS; do
if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then
echo " Found linked issue #$ISSUE_NUM"

# Get issue title
ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "")

if [ -n "$ISSUE_TITLE" ]; then
CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE"

# Check for /release-note comment on the issue
COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "")

RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "")

if [ -n "$RELEASE_NOTE" ]; then
echo " Found /release-note: $RELEASE_NOTE"
HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE"
fi
fi
fi
done
fi
fi
done

# Also check for issues closed directly (not via PR) in the commit range
# by looking at commit messages for "Fixes #XX" patterns
for SHA in $COMMIT_SHAS; do
COMMIT_MSG=$(git log -1 --pretty=format:"%B" "$SHA")
ISSUE_NUMBERS=$(echo "$COMMIT_MSG" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "")

for ISSUE_NUM in $ISSUE_NUMBERS; do
if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then
echo "Found issue #$ISSUE_NUM in commit message"

ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "")

if [ -n "$ISSUE_TITLE" ]; then
CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE"

# Check for /release-note comment
COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "")
RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "")

if [ -n "$RELEASE_NOTE" ]; then
echo " Found /release-note: $RELEASE_NOTE"
HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE"
fi
fi
fi
done
done

# Build changelog
CHANGELOG=""

# Add Highlights section if any /release-note comments exist
if [ ${#HIGHLIGHTS[@]} -gt 0 ]; then
CHANGELOG="### ✨ Highlights"$'\n\n'
for ISSUE_NUM in "${!HIGHLIGHTS[@]}"; do
CHANGELOG="${CHANGELOG}- ${HIGHLIGHTS[$ISSUE_NUM]}"$'\n'
done
CHANGELOG="${CHANGELOG}"$'\n'
fi

# Add Closed Issues section (always show if there are any)
if [ ${#CLOSED_ISSUES[@]} -gt 0 ]; then
CHANGELOG="${CHANGELOG}### 📋 Closed Issues"$'\n\n'
# Sort issue numbers for consistent ordering
for ISSUE_NUM in $(echo "${!CLOSED_ISSUES[@]}" | tr ' ' '\n' | sort -n); do
CHANGELOG="${CHANGELOG}- #${ISSUE_NUM} - ${CLOSED_ISSUES[$ISSUE_NUM]}"$'\n'
done
fi

# If changelog is empty, use a fun message
if [ -z "$CHANGELOG" ]; then
CHANGELOG="So much goodness, we lost track! 🎉"
fi

echo "Generated changelog:"
echo "$CHANGELOG"

echo "CHANGELOG<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 changes: 28 additions & 0 deletions .github/workflows/preview-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Preview Changelog

run-name: Preview release notes for next release

on:
workflow_dispatch:

jobs:
generate:
name: Generate
uses: ./.github/workflows/generate-changelog.yml

preview:
name: Display Preview
runs-on: ubuntu-latest
needs: generate

steps:
- name: Display changelog preview
run: |
echo "=========================================="
echo "CHANGELOG PREVIEW"
echo "=========================================="
echo ""
echo "## What's New in v<VERSION>"
echo ""
echo "${{ needs.generate.outputs.changelog }}"
shell: bash
84 changes: 8 additions & 76 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,19 @@ jobs:
install.ps1
retention-days: 1

changelog:
name: Generate Changelog
needs: build
uses: ./.github/workflows/generate-changelog.yml

release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: build
needs: [build, changelog]

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for changelog generation

- name: Download all build artifacts
uses: actions/download-artifact@v4
Expand All @@ -248,77 +251,6 @@ jobs:
name: install-scripts
path: .

- name: Generate changelog
id: changelog
run: |
# Get commits since last release tag
# NOTE: This must run BEFORE creating the new tag, otherwise git describe
# will find the new tag and the changelog will be empty
PREVIOUS_TAG=$(git describe --abbrev=0 --tags --match "v*" 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
# First release - get all commits
COMMITS=$(git log --pretty=format:"%s (%h)|%b" --no-merges | tr '\n' ' ')
else
# Get commits since previous tag
COMMITS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%s (%h)|%b" --no-merges | tr '\n' ' ')
fi

# Categorize commits by conventional commit type
BUGS=""
FEATURES=""
CHORES=""

# Process each commit
while IFS= read -r line; do
subject=$(echo "$line" | cut -d'|' -f1)
body=$(echo "$line" | cut -d'|' -f2-)

# Extract issue number from body if present
issue=$(echo "$body" | grep -oE "Fixes #[0-9]+" | head -1 || echo "")
if [ -n "$issue" ]; then
entry="- $subject - $issue"
else
entry="- $subject"
fi

# Categorize by prefix
if echo "$subject" | grep -qE "^fix(\(|:)"; then
BUGS="${BUGS}${entry}"$'\n'
elif echo "$subject" | grep -qE "^feat(\(|:)"; then
FEATURES="${FEATURES}${entry}"$'\n'
elif echo "$subject" | grep -qE "^chore(\(|:)"; then
CHORES="${CHORES}${entry}"$'\n'
else
# Default to chores for uncategorized
CHORES="${CHORES}${entry}"$'\n'
fi
done < <(git log ${PREVIOUS_TAG:+$PREVIOUS_TAG..}HEAD --pretty=format:"%s (%h)|%b---END---" --no-merges | sed 's/---END---/\n/g')

# Build changelog with categories
CHANGELOG=""

if [ -n "$BUGS" ]; then
CHANGELOG="${CHANGELOG}### 🐛 Bugs Squashed"$'\n\n'"${BUGS}"$'\n'
fi

if [ -n "$FEATURES" ]; then
CHANGELOG="${CHANGELOG}### 🎉 Features Added"$'\n\n'"${FEATURES}"$'\n'
fi

if [ -n "$CHORES" ]; then
CHANGELOG="${CHANGELOG}### 🧹 Chores Addressed"$'\n\n'"${CHORES}"$'\n'
fi

# If changelog is empty, use a fun message
if [ -z "$CHANGELOG" ]; then
CHANGELOG="So much goodness, we lost track! 🎉"
fi

echo "CHANGELOG<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
shell: bash

- name: Create and push release tag
run: |
VERSION="${{ github.event.inputs.version }}"
Expand Down Expand Up @@ -348,9 +280,9 @@ jobs:
install.sh
install.ps1
body: |
## Changes in v${{ github.event.inputs.version }}
## What's New in v${{ github.event.inputs.version }}

${{ steps.changelog.outputs.CHANGELOG }}
${{ needs.changelog.outputs.changelog }}

## Installation

Expand Down
Loading