Skip to content

Auto-tag and auto-release on version bump in pyproject.toml #481

@aallan

Description

@aallan

Problem

The release workflow (per CLAUDE.md) has two phases:

  1. In the feature PR: bump version across pyproject.toml, vera/__init__.py, CHANGELOG.md, HISTORY.md, docs/index.html, README.md; regenerate site assets and uv.lock.
  2. After merge: (a) tag main with vX.Y.Z, (b) create the matching GitHub release.

Phase 2 is manual and easy to forget. This just happened on v0.0.113 — #477 merged cleanly, but neither the tag nor the GitHub release was created until several PRs later when the docs-consistency audit caught the gap. For a period, docs/release history claimed v0.0.113 existed when the repo had no such tag or release.

Same class of problem as #478 (CHANGELOG enforcement): a workflow convention that depends on human memory, when it could be triggered automatically by the signal it already relies on.

Proposed solution

A GitHub Actions workflow (.github/workflows/release.yml) triggered on push to main that:

  1. Detects a version bump by checking whether the version = "X.Y.Z" line in pyproject.toml changed from the previous commit.
  2. Verifies consistency with vera/__init__.py, docs/index.html, README.md via the existing scripts/check_version_sync.py.
  3. Extracts the matching CHANGELOG entry (the ## [X.Y.Z] section) for use as the release body.
  4. Creates the tag vX.Y.Z at the merge commit SHA.
  5. Creates the GitHub release using gh release create or the softprops/action-gh-release action, with the CHANGELOG section as the body.

Pseudocode

name: Auto-release on version bump
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 2   # need previous commit for diff
      - name: Detect version bump
        id: detect
        run: |
          OLD=$(git show HEAD~1:pyproject.toml | grep -oE "version = \"[^\"]+\"" | head -1)
          NEW=$(grep -oE "version = \"[^\"]+\"" pyproject.toml | head -1)
          if [ "$OLD" != "$NEW" ]; then
            VERSION=$(echo "$NEW" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+")
            echo "version=$VERSION" >> "$GITHUB_OUTPUT"
            echo "bumped=true" >> "$GITHUB_OUTPUT"
          fi
      - name: Verify version consistency
        if: steps.detect.outputs.bumped == 'true'
        run: python scripts/check_version_sync.py
      - name: Extract release notes
        if: steps.detect.outputs.bumped == 'true'
        id: notes
        run: |
          VERSION=${{ steps.detect.outputs.version }}
          # Extract CHANGELOG section between `## [X.Y.Z]` and next `## [`
          NOTES=$(awk -v v="$VERSION" '
            $0 ~ "^## \\[" v "\\]" { flag=1; next }
            flag && /^## \[/ { exit }
            flag { print }
          ' CHANGELOG.md)
          # ... emit to GITHUB_OUTPUT with heredoc
      - name: Create tag + release
        if: steps.detect.outputs.bumped == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          VERSION=${{ steps.detect.outputs.version }}
          git tag "v$VERSION"
          git push origin "v$VERSION"
          gh release create "v$VERSION" --title "v$VERSION" --notes "$NOTES"

Safety

  • Idempotent on re-runs: the gh release create call fails fast if the tag already exists, and the version-sync check would have caught any mid-merge inconsistency before we got here.
  • No effect on non-bump merges: the detect step short-circuits cleanly when pyproject.toml's version string is unchanged.
  • Uses GITHUB_TOKEN: minimum contents: write scope; no PAT needed.

Scope

  • ~60 lines of YAML in .github/workflows/release.yml
  • Update CLAUDE.md release-workflow section to note that Phase 2 is now automatic
  • Update CONTRIBUTING.md similarly

Estimated 1–2 hours, including testing the workflow with a dry-run branch.

Related

  • #478 — sibling CI improvement for CHANGELOG enforcement at pre-push/CI
  • v0.0.113 release gap (the empirical motivation; fixed retroactively in the docs-consistency PR)

Priority

Stage 11 tooling — same category as #478. Not urgent (manual tagging is quick when remembered) but high-value per minute spent because it closes an entire class of release-hygiene misses.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ciCI/CD and GitHub Actions

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions