Skip to content

Release Workflow

This document describes the automated release process for TNH Scholar, designed to support biweekly (or faster) releases during rapid prototyping with minimal manual effort.

Quick Reference

Standard patch release workflow (e.g., x.x.y β†’ 0.x.y+1):

make release-patch       # Bump version + update TODO.md
make changelog-draft     # Generate CHANGELOG entry
# Edit CHANGELOG.md with generated content
make release-commit      # Commit version changes
make release-tag         # Tag and push to remote
make release-publish     # Strip frontmatter, build, publish to PyPI, restore README (automated)

Dry-run mode: Add DRY_RUN=1 to any command to preview without making changes:

make release-patch DRY_RUN=1    # Preview version bump
make release-commit DRY_RUN=1   # Preview commit
make release-tag DRY_RUN=1      # Preview tag and push
make release-publish DRY_RUN=1  # Preview PyPI publish

Prerequisites

One-Time Setup

Before first release, complete these configuration steps:

  1. PyPI API Token: Configure Poetry with your PyPI credentials
poetry config pypi-token.pypi <your-api-token>

Get a token from: https://pypi.org/manage/account/token/

  1. Git Configuration: Ensure git is configured with your name and email
git config user.name "Your Name"
git config user.email "your.email@example.com"
  1. Quality Checks Pass: Verify the project builds cleanly
make release-check  # Runs: test, type-check, lint, docs-verify

Pre-Release Verification

Before starting any release, ensure all quality checks pass:

make release-check

This runs:

  • make test - Full test suite with pytest
  • make type-check - MyPy type checking
  • make lint - Ruff linting
  • make docs-verify - Documentation build, link check, and spell check

Expected output: βœ… All quality checks passed - ready to release

If checks fail, fix the issues before proceeding with the release.

Release Types

Choose the appropriate version bump based on the changes:

Patch Release (0.x.Y β†’ 0.x.Y+1)

make release-patch

Use for:

  • Bug fixes
  • Documentation improvements
  • Minor refactoring without API changes
  • Dependency updates
  • Code cleanup and formatting

Minor Release (0.X.y β†’ 0.X+1.0)

make release-minor

Use for:

  • New features that maintain backward compatibility
  • Significant documentation reorganization
  • New CLI commands or subcommands
  • Major improvements to existing features

Major Release (X.y.z β†’ X+1.0.0)

make release-major

Use for:

  • Breaking API changes
  • Incompatible CLI changes
  • Major architectural shifts

Note: Major releases are rare during alpha/beta phases.

Step-by-Step Workflow

Step 1: Bump Version

Choose the appropriate version bump:

make release-patch   # For 0.1.4 β†’ 0.1.5
# OR
make release-minor   # For 0.1.4 β†’ 0.2.0
# OR
make release-major   # For 0.1.4 β†’ 1.0.0

What happens:

  • Updates pyproject.toml version field
  • Updates TODO.md version header to match
  • Displays next steps

Example output:

πŸš€ Bumping patch version (0.x.Y -> 0.x.Y+1)...
Bumping version from 0.1.4 to 0.1.5
πŸ“ Updating version to 0.1.5 in TODO.md...
βœ… Version updated to 0.1.5

Next steps:
  1. Run 'make changelog-draft' to generate CHANGELOG entry
  2. Edit CHANGELOG.md with the generated content
  3. Run 'make release-commit' to commit changes
  4. Run 'make release-tag' to tag and push
  5. Run 'make release-publish' to publish to PyPI

Step 2: Generate CHANGELOG Entry

make changelog-draft

What happens:

  • Analyzes git commits since the last tag
  • Categorizes commits by type (Added, Changed, Fixed, Documentation, etc.)
  • Generates a formatted CHANGELOG entry

Example output:

πŸ“ Generating CHANGELOG entry from git history...

## [0.1.5] - 2025-12-06

### Added

- Version sync pre-commit hook to prevent version mismatches
- Python-based link checker (md-dead-link-check) for documentation

### Changed

- Replaced lychee with Python-native link checking tool
- Improved developer onboarding with fewer external dependencies

### Documentation

- Created comprehensive release workflow documentation

============================================================
πŸ“ Draft CHANGELOG entry for v0.1.5
Based on 8 commits since v0.1.4
============================================================

πŸ‘† Copy this to CHANGELOG.md and edit as needed

Step 3: Edit CHANGELOG.md

  1. Open CHANGELOG.md (in project root) in your editor
  2. Copy the generated entry from your terminal
  3. Paste at the top of the file (after the header, before previous versions)
  4. Edit the entry for clarity and completeness:
  5. Remove noise commits (merge commits, trivial fixes)
  6. Rewrite commit messages for user-facing clarity
  7. Group related changes together
  8. Add context or references where needed
  9. Add "Notes" or "Breaking Changes" sections if applicable
  10. Save the file

Example polished entry:

# Changelog

All notable changes to TNH Scholar will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [0.1.5] - 2025-12-06

### Added

- **Version Sync Pre-commit Hook**: Automatically validates that pyproject.toml and TODO.md versions match before allowing commits
- **Python-based Link Checker**: Replaced lychee (Rust tool) with md-dead-link-check for pure Python toolchain

### Changed

- Simplified developer setup by removing non-Python tooling requirements
- Restored full documentation verification pipeline with Python-native tools

### Documentation

- Created comprehensive release workflow documentation in docs/development/

### Notes

- This release focuses on release automation improvements (Phase 2)
- No code functionality changes - purely tooling and documentation infrastructure
- External link checking now uses pure Python stack for better Poetry integration

## [0.1.4] - 2025-12-05
...

Step 4: Commit Version Changes

make release-commit

What happens:

  • Stages pyproject.toml, TODO.md, CHANGELOG.md, and poetry.lock
  • Creates a formatted commit with a standard message
  • Shows next steps

Commit message format:

chore: Bump version to 0.1.5

- Update version in pyproject.toml
- Update TODO.md version header
- Add 0.1.5 release notes to CHANGELOG.md

πŸ€– Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

Step 5: Tag and Push

make release-tag

What happens:

  • Creates an annotated git tag (e.g., v0.1.5)
  • Pushes the current branch to the remote
  • Pushes the tag to the remote

Tag message format:

Release v0.1.5

See CHANGELOG.md for full details.

πŸ€– Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

Example output:

🏷️  Tagging version v0.1.5...
πŸ“€ Pushing branch and tag...
βœ… Tagged and pushed v0.1.5

Next: Run 'make release-publish' to publish to PyPI

Step 6: Publish to PyPI

make release-publish

What happens (all automated):

  1. Prepares README for PyPI: Strips YAML frontmatter to ensure clean rendering
  2. Creates backup at README.md.bak
  3. Removes frontmatter that would display as plain text on PyPI

  4. Builds distributions: Creates wheel and source distribution using Poetry

  5. Publishes to PyPI: Uploads both distributions

  6. Restores README: Automatically restores original README with frontmatter

Why frontmatter stripping is needed: PyPI doesn't process YAML frontmatter and will display it as plain text, making your project description look unprofessional. The automation handles this for you.

Example output:

πŸ“ Preparing README for PyPI (stripping YAML frontmatter)...
βœ“ Backed up README.md to /path/to/README.md.bak
βœ“ Stripped 443 bytes of frontmatter from README.md

πŸ“¦ README.md is ready for PyPI build
πŸ’‘ Run 'python scripts/prepare_pypi_readme.py --restore' to restore original

πŸ“¦ Building package...
Building tnh-scholar (0.1.5)
  - Building sdist
  - Built tnh_scholar-0.1.5.tar.gz
  - Building wheel
  - Built tnh_scholar-0.1.5-py3-none-any.whl

πŸ“€ Publishing to PyPI...
Publishing tnh-scholar (0.1.5) to PyPI
 - Uploading tnh_scholar-0.1.5-py3-none-any.whl 100%
 - Uploading tnh_scholar-0.1.5.tar.gz 100%

πŸ“ Restoring original README...
βœ“ Restored README.md from backup

βœ… Published v0.1.5 to PyPI

πŸŽ‰ Release complete! Check https://pypi.org/project/tnh-scholar/

Step 7: Verify Release

After publishing, verify the release was successful:

  1. Check PyPI: Visit https://pypi.org/project/tnh-scholar/
  2. Verify the new version is listed
  3. Important: Check that README renders correctly without YAML frontmatter
  4. Confirm the project description starts with "# TNH Scholar README" and not with "---"

  5. Test Installation:

pip install --upgrade tnh-scholar==0.1.5
python -c "import tnh_scholar; print(tnh_scholar.__version__)"

Expected output: 0.1.5

  1. Check GitHub:
  2. Tag appears in releases: https://github.com/aaronksolomon/tnh-scholar/tags
  3. Verify the commit and tag messages are correct

Advanced Usage

All-in-One Release

If you've already edited CHANGELOG.md and are confident in your changes:

make release-full

This runs: release-commit β†’ release-tag β†’ release-publish in sequence.

⚠️ Use with caution: This skips intermediate verification steps. Only use this after you've verified CHANGELOG.md is correct.

Dry Run Mode

Preview any release command without making changes by adding DRY_RUN=1:

# Preview version bump
make release-patch DRY_RUN=1

# Preview commit
make release-commit DRY_RUN=1

# Preview tag and push
make release-tag DRY_RUN=1

# Preview PyPI publish
make release-publish DRY_RUN=1

Example dry-run output for version bump:

πŸ” DRY RUN MODE - No changes will be made

πŸš€ Would bump patch version: 0.1.4 β†’ 0.1.5

Commands that would run:
  poetry version patch
  sed -i.bak 's/> \*\*Version\*\*:.*/> **Version**: 0.1.5 (Alpha)/' TODO.md

To execute: make release-patch

Benefits:

  • See exact commands that will run
  • Verify commit messages and tag messages before creating them
  • Catch errors early (wrong version bump, missing files, etc.)
  • Safe way to learn the release workflow
  • Useful for documenting the process

When to use dry-run:

  • First time using a release target
  • Testing a new release workflow
  • Verifying what will be committed/tagged/published
  • Training new contributors
  • Before major releases

Test version bumps without dry-run:

# Check current version
poetry version -s

# Test version bump calculation (doesn't modify files)
poetry version --dry-run patch   # Shows: 0.1.4 β†’ 0.1.5
poetry version --dry-run minor   # Shows: 0.1.4 β†’ 0.2.0

# Actually bump (modifies pyproject.toml)
make release-patch

# Undo if needed (before committing)
git checkout pyproject.toml TODO.md poetry.lock

Skip Steps

You can run individual targets if you need to customize the workflow:

# Just generate changelog (no version bump)
make changelog-draft

# Just commit (if you bumped version manually)
make release-commit

# Just tag (if you committed manually)
make release-tag

# Just publish (if you tagged manually)
make release-publish

Automation Features

Version Sync Pre-commit Hook

A pre-commit hook validates that pyproject.toml and TODO.md versions match before allowing commits. This prevents version drift bugs.

What it checks:

  • Reads version from pyproject.toml via poetry version -s
  • Extracts version from TODO.md using pattern matching
  • Fails commit if versions don't match

How to fix a mismatch:

# Use one of the release targets to sync versions
make release-patch   # Bump patch version
make release-minor   # Bump minor version
make release-major   # Bump major version

The hook runs automatically on every commit. You can also run it manually:

poetry run pre-commit run version-sync --all-files

Documentation Verification

The make docs-verify target runs a comprehensive documentation quality check:

  1. Drift Check: Ensures README.md and docs/index.md are in sync
  2. Build Check: Runs MkDocs in strict mode (fails on warnings)
  3. Link Check: Validates external links using md-dead-link-check (Python-native)
  4. Spell Check: Runs codespell on README and docs

All documentation tools are Python-based and managed by Poetry for consistent developer experience.

Troubleshooting

"No previous tag found"

Problem: First release, no git tags exist yet.

Solution: Write the CHANGELOG entry manually, skip make changelog-draft.

"Version mismatch in TODO.md"

Problem: TODO.md version marker not found or malformed.

Solution: Ensure TODO.md contains a line matching the pattern:

> **Version**: 0.1.4 (Alpha)

The version sync hook validates this format.

"PyPI authentication failed"

Problem: No PyPI API token configured.

Solution:

poetry config pypi-token.pypi <your-token>

Get a token from: https://pypi.org/manage/account/token/

"Tests failing in release-check"

Problem: Code has failing tests, type errors, or lint violations.

Solution: Fix issues before releasing:

make test           # Run pytest test suite
make type-check     # Run mypy type checks
make lint           # Run ruff linting
make docs-verify    # Build and validate docs

Address all failures before proceeding with the release.

"Git push rejected"

Problem: Remote has changes you don't have locally.

Solution:

git pull --rebase origin $(git branch --show-current)
# Resolve conflicts if any
make release-tag  # Try again

Problem: External links are broken or timing out.

Solution:

  1. Review the failing links in the output
  2. Update or remove broken links
  3. For transient failures, retry after a few minutes
  4. For sites that block automated requests, add to the exclude list in pyproject.toml:
[tool.md_dead_link_check]
exclude_links = [
    "https://example.com/*",  # Add problematic domains
]

Tips for Efficient Releases

Use Conventional Commits

Format commits to make CHANGELOG generation more accurate:

git commit -m "feat: add new feature X"
git commit -m "fix: resolve bug in Y"
git commit -m "docs: update getting started guide"
git commit -m "chore: bump dependencies"

Commit prefixes:

  • feat: β†’ Added section
  • fix: β†’ Fixed section
  • docs: β†’ Documentation section
  • chore:, build:, ci: β†’ Infrastructure section
  • refactor:, perf: β†’ Changed section
  • test: β†’ Testing section

For rapid iteration, you can release after any logical batch of commits:

# Week 1: Development
git commit -m "feat: implement feature A"
git commit -m "feat: implement feature B"
git commit -m "fix: resolve edge case in feature A"
git commit -m "docs: document features A and B"

# Week 2: Release day
make release-patch
make changelog-draft
# Edit CHANGELOG.md
make release-commit
make release-tag
make release-publish

# Total time: 10-15 minutes

Schedule Regular Releases

Consider a biweekly release cadence during active development:

  • Every other Friday: Release day
  • Morning: Run make release-check to catch issues early
  • Afternoon: Execute release workflow
  • Result: Consistent, predictable releases that build user trust

Parallel Development

You can work on multiple features in parallel using branches:

# Create feature branches from stable release
git checkout -b feature-a v0.1.4
# ... work on feature A ...

git checkout -b feature-b v0.1.4
# ... work on feature B ...

# When ready, merge to main and release
git checkout main
git merge feature-a
make release-patch  # Release 0.1.5

This allows rapid iteration without blocking work on new features.

Release Checklist

Use this checklist to ensure complete releases:

  • All quality checks pass (make release-check)
  • Version bumped with appropriate type (make release-patch/minor/major)
  • CHANGELOG.md updated with clear, user-friendly descriptions
  • Version changes committed (make release-commit)
  • Tag created and pushed (make release-tag)
  • Package published to PyPI (make release-publish - automatically strips/restores README frontmatter)
  • PyPI listing verified at https://pypi.org/project/tnh-scholar/
  • Installation test from PyPI successful (pip install tnh-scholar==X.Y.Z)
  • GitHub tag appears in repository tags
  • GitHub Release created with release notes
  • Makefile (project root) - Release automation implementation
  • CHANGELOG.md (project root) - Version history
  • Contributing Guide - Contribution guidelines
  • Markdown Standards - Documentation standards