name: Automated Version Bump
# This workflow automatically creates version bump PRs based on conventional commits.
#
# DUPLICATE PREVENTION:
# - Automatically checks for existing version-bump PRs before creating new ones
# - Manual triggering is prevented if open version-bump PRs exist
# - Use 'override_check: true' only if you need to force creation despite existing PRs
#
# TRIGGERS:
# - Automatic: Push to main (if changes warrant a version bump)
# - Scheduled: Weekly on Mondays at 9 AM UTC
# - Manual: workflow_dispatch (with duplicate prevention)
on:
push:
branches: [main]
schedule:
# Run weekly on Monday at 9 AM UTC
- cron: '0 9 * * 1'
workflow_dispatch:
inputs:
force_bump:
description: 'â ī¸ Force version bump even if no significant changes'
required: false
default: false
type: boolean
bump_type:
description: 'Override automatic version detection (usually use "auto")'
required: false
default: 'auto'
type: choice
options:
- auto
- patch
- minor
- major
override_check:
description: 'đ¨ Override existing PR check - ONLY if you need to create duplicate PR'
required: false
default: false
type: boolean
permissions:
contents: write
pull-requests: write
issues: write
jobs:
analyze-changes:
name: Analyze Changes
runs-on: ubuntu-latest
outputs:
needs-bump: ${{ steps.analysis.outputs.needs-bump }}
bump-type: ${{ steps.analysis.outputs.bump-type }}
new-version: ${{ steps.analysis.outputs.new-version }}
changelog: ${{ steps.analysis.outputs.changelog }}
commits-count: ${{ steps.analysis.outputs.commits-count }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get latest tag
id: latest-tag
run: |
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "latest-tag=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Latest tag: $LATEST_TAG"
- name: Analyze commits since last release
id: analysis
run: |
python scripts/analyze-version.py \
"${{ steps.latest-tag.outputs.latest-tag }}" \
"${{ inputs.force_bump || 'false' }}" \
"${{ inputs.bump_type || 'auto' }}"
create-version-bump:
name: Create Version Bump PR
runs-on: ubuntu-latest
needs: analyze-changes
if: needs.analyze-changes.outputs.needs-bump == 'true'
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
pip install tomli_w
- name: Check for existing version bump PRs
id: check-existing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Check if there are any open version-bump PRs
EXISTING_PRS=$(gh pr list --search "is:open head:version-bump/" --json number,headRefName --jq length)
if [ "$EXISTING_PRS" -gt 0 ] && [ "${{ inputs.override_check }}" != "true" ]; then
echo "â Found $EXISTING_PRS existing version bump PR(s)"
gh pr list --search "is:open head:version-bump/" --json number,title,headRefName
echo ""
echo "đĄ To override this check, manually trigger with 'override_check: true'"
echo "â Aborting to prevent duplicate version bump PRs"
exit 1
elif [ "$EXISTING_PRS" -gt 0 ]; then
echo "â ī¸ Found existing PRs but override_check=true, continuing..."
else
echo "â
No existing version bump PRs found, proceeding..."
fi
- name: Create version bump branch
run: |
NEW_VERSION="${{ needs.analyze-changes.outputs.new-version }}"
BRANCH_NAME="version-bump/v$NEW_VERSION"
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
# Delete local branch if it exists
git branch -D "$BRANCH_NAME" 2>/dev/null || true
# Create fresh branch from main
git checkout -b "$BRANCH_NAME"
echo "branch-name=$BRANCH_NAME" >> $GITHUB_ENV
- name: Update version in pyproject.toml
run: |
NEW_VERSION="${{ needs.analyze-changes.outputs.new-version }}"
python -c "
import tomllib
import tomli_w
# Read current pyproject.toml
with open('pyproject.toml', 'rb') as f:
data = tomllib.load(f)
# Update version
data['project']['version'] = '$NEW_VERSION'
# Write back
with open('pyproject.toml', 'wb') as f:
tomli_w.dump(data, f)
print(f'Updated version to $NEW_VERSION in pyproject.toml')
"
- name: Update CHANGELOG.md
run: |
NEW_VERSION="${{ needs.analyze-changes.outputs.new-version }}"
CHANGELOG="${{ needs.analyze-changes.outputs.changelog }}"
# Create new changelog entry
cat > new_entry.md << 'EOF'
## [$NEW_VERSION] - $(date +%Y-%m-%d)
$CHANGELOG
EOF
# Backup current changelog
cp CHANGELOG.md CHANGELOG.md.bak
# Create new changelog
{
echo "# Changelog"
echo ""
echo "All notable changes to this project will be documented in this file."
echo ""
echo "The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),"
echo "and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)."
echo ""
# Add new entry
envsubst < new_entry.md
echo ""
# Add existing content (skip header)
tail -n +9 CHANGELOG.md.bak
} > CHANGELOG.md
# Clean up
rm new_entry.md CHANGELOG.md.bak
- name: Commit changes
run: |
NEW_VERSION="${{ needs.analyze-changes.outputs.new-version }}"
BUMP_TYPE="${{ needs.analyze-changes.outputs.bump-type }}"
git add pyproject.toml CHANGELOG.md
# Create conventional commit message
git commit -m "chore: bump version to v$NEW_VERSION
This $BUMP_TYPE version bump includes:
- ${{ needs.analyze-changes.outputs.commits-count }} commits since last release
- Automated changelog generation
- Version update in pyproject.toml
Release will be triggered automatically when this PR is merged."
- name: Push branch and create PR
run: |
NEW_VERSION="${{ needs.analyze-changes.outputs.new-version }}"
BUMP_TYPE="${{ needs.analyze-changes.outputs.bump-type }}"
BRANCH_NAME="${{ env.branch-name }}"
# Force push branch (overwrite any existing branch)
git push --force origin "$BRANCH_NAME"
# Create PR description
cat > pr_body.md << EOF
## đ Automated Version Bump: v$NEW_VERSION
This PR automatically bumps the version based on conventional commits since the last release.
### đ Release Summary
- **Version Type**: $BUMP_TYPE
- **New Version**: v$NEW_VERSION
- **Commits Included**: ${{ needs.analyze-changes.outputs.commits-count }}
- **Generated**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')
### đ Changelog Preview
${{ needs.analyze-changes.outputs.changelog }}
### đ¯ What Happens Next
1. **Review**: Maintainers review this automated version bump
2. **Merge**: When merged, a git tag \`v$NEW_VERSION\` will be created
3. **Release**: The tag will trigger the automated release workflow
4. **Publish**: Package will be published to PyPI automatically
### â
Pre-Release Checklist
- [ ] Version number looks correct
- [ ] Changelog entries are accurate
- [ ] No breaking changes in patch/minor releases
- [ ] All CI checks pass
---
*This PR was created automatically by the version bump workflow. The release will be fully automated upon merge.*
EOF
# Check if PR already exists
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number' || echo "")
if [ -n "$EXISTING_PR" ]; then
echo "PR #$EXISTING_PR already exists, updating it..."
# Update existing PR
gh pr edit "$EXISTING_PR" \
--title "chore: bump version to v$NEW_VERSION" \
--body-file pr_body.md
echo "â
Version bump PR #$EXISTING_PR updated successfully!"
else
# Try to create new PR
if gh pr create \
--title "chore: bump version to v$NEW_VERSION" \
--body-file pr_body.md \
--base main \
--head "$BRANCH_NAME" \
--label "release" \
--label "automated" 2>/dev/null; then
echo "â
Version bump PR created successfully!"
else
echo "â ī¸ Could not create PR automatically due to repository permissions."
echo "đ The branch 'version-bump/v$NEW_VERSION' has been pushed."
echo ""
echo "đ Please create the PR manually:"
echo " 1. Go to: https://github.com/${{ github.repository }}/pull/new/$BRANCH_NAME"
echo " 2. Use this title: 'chore: bump version to v$NEW_VERSION'"
echo " 3. Copy the description from below"
echo ""
echo "đ PR Description:"
echo "---"
cat pr_body.md
echo "---"
echo ""
echo "Alternatively, enable 'Allow GitHub Actions to create and approve pull requests' in:"
echo "Settings â Actions â General â Workflow permissions"
fi
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
auto-tag-on-merge:
name: Auto-tag on Merge
runs-on: ubuntu-latest
if: >
github.event_name == 'push' &&
github.ref == 'refs/heads/main' &&
contains(github.event.head_commit.message, 'chore: bump version to v')
steps:
- uses: actions/checkout@v4
- name: Extract version from commit message
id: version
run: |
COMMIT_MESSAGE="${{ github.event.head_commit.message }}"
VERSION=$(echo "$COMMIT_MESSAGE" | grep -oP 'chore: bump version to v\K[0-9]+\.[0-9]+\.[0-9]+' || echo "")
if [ -z "$VERSION" ]; then
echo "â Could not extract version from commit message"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "â
Extracted version: $VERSION"
- name: Create and push tag
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="v$VERSION"
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
# Create signed tag
git tag -a "$TAG" -m "Release $TAG
This release was automatically created from version bump commit.
See CHANGELOG.md for details of changes included in this release."
git push origin "$TAG"
echo "â
Created and pushed tag: $TAG"
notify-no-changes:
name: No Changes Notification
runs-on: ubuntu-latest
needs: analyze-changes
if: needs.analyze-changes.outputs.needs-bump == 'false' && github.event_name != 'schedule'
steps:
- name: Notify no version bump needed
run: |
echo "âšī¸ No version bump needed"
echo "No significant changes found since last release."
echo "Commits analyzed: ${{ needs.analyze-changes.outputs.commits-count }}"
echo ""
echo "To force a version bump, re-run this workflow with 'force_bump' enabled."