Skip to content
Draft
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
85 changes: 85 additions & 0 deletions .github/workflows/README_update_release_notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Automated Release Notes Workflow

This document explains how the automated release notes generation workflow works.

## Overview

The `update_release_notes.yml` workflow automatically updates the `release.md` file whenever:
- Commits are pushed to the `main` branch
- Pull requests are opened, synchronized, or reopened targeting the `main` branch

## How It Works

### Commit Range Detection

- **For Pull Requests**: Analyzes commits between the PR base and head
- **For Push Events**: Analyzes commits since the last tag, or last 50 commits if no tags exist

### Categorization

The workflow categorizes commits based on conventional commit prefixes:

- **πŸš€ Features**: Commits starting with `feature:` or `feat:`
- **πŸ§ͺ Tests**: Commits starting with `test:`
- **πŸ“š Documentation**: Commits starting with `docs:`
- **βš™οΈ Build & Compatibility**: Commits starting with `build:` or `ci:`
- **πŸ› Bugfixes**: Commits starting with `fix:` or `bugfix:`
- **🎨 Style**: Commits starting with `style:`

### Release Notes Structure

The workflow generates an "Unreleased" section at the top of `release.md` with:

1. **New Features**: List of feature commits
2. **Other Tag Highlights**: Organized by category (Tests, Docs, Build)
3. **Bugfixes**: List of bugfix commits
4. **Full Changelog**: Statistics table showing commit distribution and complete commit list

### Workflow Behavior

1. **On Push to Main**: The workflow commits and pushes the updated `release.md` directly to the main branch
2. **On Pull Request**: The workflow commits the updated `release.md` to the PR branch

The commit message includes `[skip ci]` for push events to prevent triggering the workflow recursively.

## Using Conventional Commit Messages

To take full advantage of automatic categorization, use conventional commit prefixes:

```bash
# Examples
git commit -m "feat: add new GPU acceleration support"
git commit -m "fix: resolve memory leak in solver"
git commit -m "docs: update installation guide"
git commit -m "test: add unit tests for geometry module"
git commit -m "style: format code with black"
git commit -m "build: update numpy dependency"
```

## Manual Release Process

When you're ready to publish a new release:

1. Review the "Unreleased" section in `release.md`
2. Manually edit it to:
- Change "# Unreleased" to "# v{version}"
- Add a descriptive summary of the release
- Organize and expand the automatically generated content
- Add any additional sections (e.g., "New Contributors")
3. Commit the changes
4. Tag the release using `release.sh` or manually

The next time the workflow runs, it will create a new "Unreleased" section above your versioned release.

## Disabling the Workflow

If you need to temporarily disable automatic release notes:

1. Rename the workflow file or move it to a different directory
2. Or add a condition to skip the workflow in certain cases

## Troubleshooting

- **No changes detected**: The workflow only commits if there are actual changes to `release.md`
- **Permission errors**: Ensure the workflow has `contents: write` permission
- **Commit range issues**: For repositories with shallow clones, increase fetch-depth in checkout action
239 changes: 239 additions & 0 deletions .github/workflows/update_release_notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
name: Update Release Notes

on:
push:
branches:
- main
pull_request:
branches:
- main
types: [opened, synchronize, reopened]

jobs:
update-release-notes:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper changelog generation
ref: ${{ github.head_ref || github.ref_name }}

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Generate release notes
run: |
# Get the version from setup.py
VERSION=$(python setup.py --version 2>/dev/null || echo "dev")

# Determine the commit range
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="${{ github.event.pull_request.base.sha }}"
HEAD_REF="${{ github.event.pull_request.head.sha }}"
COMMIT_RANGE="${BASE_REF}..${HEAD_REF}"
else
# For push events, get the last tag and use commits since then
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
COMMIT_RANGE="${LAST_TAG}..HEAD"
else
# If no tags, use last 50 commits
COMMIT_RANGE="HEAD~50..HEAD" 2>/dev/null || COMMIT_RANGE="HEAD"
fi
fi

echo "Processing commits in range: ${COMMIT_RANGE}"

# Count total commits in range
TOTAL_COMMITS=$(git log ${COMMIT_RANGE} --oneline 2>/dev/null | wc -l || echo 0)

if [ ${TOTAL_COMMITS} -eq 0 ]; then
echo "No new commits to process"
exit 0
fi

# Create a temporary file for the unreleased section
cat > /tmp/unreleased_section.md << 'EOF'
# Unreleased

This section contains updates that will be included in the next release.

---

## πŸš€ New Features

EOF

# Extract feature commits
git log ${COMMIT_RANGE} --date=short --pretty=format:"* %ad %s (%aN)" --grep="^feature:" --grep="^feat:" -i 2>/dev/null > /tmp/features.txt || true
if [ -s /tmp/features.txt ]; then
cat /tmp/features.txt >> /tmp/unreleased_section.md
else
echo "* No new features yet" >> /tmp/unreleased_section.md
fi

echo "" >> /tmp/unreleased_section.md
echo "---" >> /tmp/unreleased_section.md
echo "" >> /tmp/unreleased_section.md
echo "## πŸ’— Other Tag Highlights" >> /tmp/unreleased_section.md
echo "" >> /tmp/unreleased_section.md
echo "* πŸ” **Tests**" >> /tmp/unreleased_section.md

# Extract test commits
git log ${COMMIT_RANGE} --date=short --pretty=format:" * %ad %s (%aN)" --grep="^test:" -i 2>/dev/null > /tmp/tests.txt || true
if [ -s /tmp/tests.txt ]; then
cat /tmp/tests.txt >> /tmp/unreleased_section.md
else
echo " * No test updates" >> /tmp/unreleased_section.md
fi

echo "" >> /tmp/unreleased_section.md
echo "* πŸ“š **Documentation**" >> /tmp/unreleased_section.md

# Extract doc commits
git log ${COMMIT_RANGE} --date=short --pretty=format:" * %ad %s (%aN)" --grep="^docs:" -i 2>/dev/null > /tmp/docs.txt || true
if [ -s /tmp/docs.txt ]; then
cat /tmp/docs.txt >> /tmp/unreleased_section.md
else
echo " * No documentation updates" >> /tmp/unreleased_section.md
fi

echo "" >> /tmp/unreleased_section.md
echo "* βš™οΈ **Build & Compatibility**" >> /tmp/unreleased_section.md

# Extract build commits
git log ${COMMIT_RANGE} --date=short --pretty=format:" * %ad %s (%aN)" --grep="^build:" --grep="^ci:" -i 2>/dev/null > /tmp/build.txt || true
if [ -s /tmp/build.txt ]; then
cat /tmp/build.txt >> /tmp/unreleased_section.md
else
echo " * No build updates" >> /tmp/unreleased_section.md
fi

echo "" >> /tmp/unreleased_section.md
echo "---" >> /tmp/unreleased_section.md
echo "" >> /tmp/unreleased_section.md
echo "## πŸ› **Bugfixes**" >> /tmp/unreleased_section.md
echo "" >> /tmp/unreleased_section.md

# Extract bugfix commits
git log ${COMMIT_RANGE} --date=short --pretty=format:"* %ad %s (%aN)" --grep="^fix:" --grep="^bugfix:" -i 2>/dev/null > /tmp/fixes.txt || true
if [ -s /tmp/fixes.txt ]; then
cat /tmp/fixes.txt >> /tmp/unreleased_section.md
else
echo "* No bugfixes yet" >> /tmp/unreleased_section.md
fi

echo "" >> /tmp/unreleased_section.md
echo "---" >> /tmp/unreleased_section.md
echo "" >> /tmp/unreleased_section.md
echo "## πŸ“ **Full changelog**" >> /tmp/unreleased_section.md
echo "" >> /tmp/unreleased_section.md

# Count commits by category
FEAT_COUNT=$(git log ${COMMIT_RANGE} --oneline --grep="^feature:" --grep="^feat:" -i 2>/dev/null | wc -l || echo 0)
TEST_COUNT=$(git log ${COMMIT_RANGE} --oneline --grep="^test:" -i 2>/dev/null | wc -l || echo 0)
DOCS_COUNT=$(git log ${COMMIT_RANGE} --oneline --grep="^docs:" -i 2>/dev/null | wc -l || echo 0)
FIX_COUNT=$(git log ${COMMIT_RANGE} --oneline --grep="^fix:" --grep="^bugfix:" -i 2>/dev/null | wc -l || echo 0)
STYLE_COUNT=$(git log ${COMMIT_RANGE} --oneline --grep="^style:" -i 2>/dev/null | wc -l || echo 0)

if [ ${TOTAL_COMMITS} -gt 0 ]; then
FEAT_PCT=$(awk "BEGIN {printf \"%.1f\", (${FEAT_COUNT}/${TOTAL_COMMITS})*100}")
TEST_PCT=$(awk "BEGIN {printf \"%.1f\", (${TEST_COUNT}/${TOTAL_COMMITS})*100}")
DOCS_PCT=$(awk "BEGIN {printf \"%.1f\", (${DOCS_COUNT}/${TOTAL_COMMITS})*100}")
FIX_PCT=$(awk "BEGIN {printf \"%.1f\", (${FIX_COUNT}/${TOTAL_COMMITS})*100}")
STYLE_PCT=$(awk "BEGIN {printf \"%.1f\", (${STYLE_COUNT}/${TOTAL_COMMITS})*100}")
OTHER_COUNT=$((TOTAL_COMMITS - FEAT_COUNT - TEST_COUNT - DOCS_COUNT - FIX_COUNT - STYLE_COUNT))
OTHER_PCT=$(awk "BEGIN {printf \"%.1f\", (${OTHER_COUNT}/${TOTAL_COMMITS})*100}")
else
FEAT_PCT=0
TEST_PCT=0
DOCS_PCT=0
FIX_PCT=0
STYLE_PCT=0
OTHER_PCT=0
fi

cat >> /tmp/unreleased_section.md << EOF
| **${TOTAL_COMMITS} commits** | πŸ“š Docs | πŸ§ͺ Tests | πŸ› Fixes | 🎨 Style | ✨ Features | Other |
|-----------------|---------|----------|-----------|------------|--------------|-------|
| % of Commits | ${DOCS_PCT}% | ${TEST_PCT}% | ${FIX_PCT}% | ${STYLE_PCT}% | ${FEAT_PCT}% | ${OTHER_PCT}% |

EOF

echo "" >> /tmp/unreleased_section.md
echo '`git log --date=short --pretty=format:"* %ad %s (%aN)"`' >> /tmp/unreleased_section.md
echo "" >> /tmp/unreleased_section.md
echo "" >> /tmp/unreleased_section.md

# Add all commits
git log ${COMMIT_RANGE} --date=short --pretty=format:"* %ad %s (%aN)" 2>/dev/null >> /tmp/unreleased_section.md || true

echo "" >> /tmp/unreleased_section.md
echo "" >> /tmp/unreleased_section.md

# Update release.md
if [ -f release.md ]; then
# Check if there's already an Unreleased section and remove it
if grep -q "^# Unreleased" release.md; then
# Find the line number of the first released version (starts with # v)
FIRST_VERSION_LINE=$(grep -n "^# v[0-9]" release.md | head -1 | cut -d: -f1)
if [ -n "$FIRST_VERSION_LINE" ]; then
# Keep everything from the first version onwards
tail -n +${FIRST_VERSION_LINE} release.md > /tmp/existing_releases.md
# Prepend new unreleased section
cat /tmp/unreleased_section.md > release.md
echo "---" >> release.md
echo "" >> release.md
cat /tmp/existing_releases.md >> release.md
else
# No versioned releases found, just replace with unreleased
mv /tmp/unreleased_section.md release.md
fi
else
# No unreleased section exists, prepend to existing content
cat /tmp/unreleased_section.md > /tmp/updated_release.md
echo "---" >> /tmp/updated_release.md
echo "" >> /tmp/updated_release.md
cat release.md >> /tmp/updated_release.md
mv /tmp/updated_release.md release.md
fi
else
# No release.md exists, create it
mv /tmp/unreleased_section.md release.md
fi

- name: Check for changes
id: check_changes
run: |
if git diff --quiet release.md; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No changes to release.md"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "Changes detected in release.md"
fi

- name: Commit and push changes
if: steps.check_changes.outputs.has_changes == 'true' && github.event_name == 'push'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add release.md
git commit -m "chore: auto-update release.md [skip ci]"
git push

- name: Commit changes to PR
if: steps.check_changes.outputs.has_changes == 'true' && github.event_name == 'pull_request'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add release.md
git commit -m "chore: auto-update release.md for PR #${{ github.event.pull_request.number }}"
git push origin HEAD:${{ github.head_ref }}
Loading