Release Strategy & Cadence
This document explains when and how to create releases for GraphForge.
- Merge to main frequently (small PRs, continuous integration)
- Release periodically when enough value accumulates
- Don’t release every merge - batch changes for meaningful releases
- Use labels to track what goes in next release
Release Cadence
Section titled “Release Cadence”Regular Release Schedule
Section titled “Regular Release Schedule”GraphForge follows a time-based + feature-based release strategy:
Minor Releases (0.x.0):
- Frequency: Every 3-4 weeks
- Contains: New features, accumulated bug fixes, improvements
- Triggered by: Significant features complete OR scheduled date
Patch Releases (0.x.y):
- Frequency: As needed, typically 1-2 weeks after minor release
- Contains: Bug fixes, documentation updates, small improvements
- Triggered by: Critical bugs OR enough fixes accumulated
Hotfix Releases (immediate):
- Frequency: As soon as possible
- Contains: Critical bug fixes only
- Triggered by: Security issues, data loss bugs, breaking bugs
When NOT to Release
Section titled “When NOT to Release”❌ Don’t release for:
- Single small bug fix (unless critical)
- Documentation-only changes
- Internal refactoring with no user impact
- CI/CD configuration changes
- Dependency updates (unless they fix vulnerabilities)
✅ Instead: Accumulate these changes and release together
Decision Matrix: Should You Release?
Section titled “Decision Matrix: Should You Release?”Release Now ✅
Section titled “Release Now ✅”If ANY of these are true:
-
Critical bug fix
- Security vulnerability
- Data loss or corruption
- Complete feature breakage
- → Hotfix release (patch bump)
-
Major feature complete
- Significant new capability (OPTIONAL MATCH, variable-length paths)
- Large improvement to existing feature
- → Minor release
-
Scheduled release date
- 3-4 weeks since last release
- Multiple changes accumulated
- → Minor or patch release
-
Breaking change needed
- API change required
- → Major release (or 0.x.0 if pre-1.0)
Accumulate Changes 📦
Section titled “Accumulate Changes 📦”If ALL of these are true:
- No critical bugs
- No major features complete
- Less than 2 weeks since last release
- Changes are small fixes or improvements
→ Merge to main, add to CHANGELOG [Unreleased], wait for more changes
PR Lifecycle
Section titled “PR Lifecycle”1. PR Created
Section titled “1. PR Created”Add label indicating release impact:
release:patch- Bug fix, small improvementrelease:minor- New feature, significant changerelease:major- Breaking changerelease:none- Docs, CI, internal only (no release needed)
2. PR Merged to Main
Section titled “2. PR Merged to Main”# PR merges to maingit checkout maingit merge pr-branch
# Update CHANGELOG [Unreleased] section# NO release created yet - just merged to mainMain branch is always deployable but not always released.
3. Accumulation Phase
Section titled “3. Accumulation Phase”Multiple PRs merge over days/weeks:
main: [PR] ── [PR] ── [PR] ── [PR] │ │ │ │ patch docs patch featureUnreleased section in CHANGELOG.md grows:
## [Unreleased]
### Added- Variable-length path support
### Fixed- Fixed column naming in aggregations- Fixed null handling in WHERE clause
### Changed- Updated documentation for WITH clause4. Release Decision
Section titled “4. Release Decision”Release when:
- Enough changes accumulated (5-10 items in [Unreleased])
- Major feature complete
- Scheduled date reached
- Critical bug needs immediate fix
Trigger release:
# Review [Unreleased] sectioncat CHANGELOG.md
# Decide: minor (feature) or patch (fixes only)python scripts/bump_version.py minor
# Complete release process (see RELEASING.md)git commit -am "chore(release): bump version to 0.2.0"git push origin maingit tag -a v0.2.0 -m "Release version 0.2.0"git push origin v0.2.0gh release create v0.2.0This triggers PyPI publish automatically.
PyPI Publishing
Section titled “PyPI Publishing”Automatic Publishing
Section titled “Automatic Publishing”PyPI publish is automated via GitHub Actions:
on: release: types: [published]Workflow:
- Create GitHub Release → Triggers workflow
- Workflow runs
uv build→ Creates wheel and sdist - Workflow runs
uv publish→ Uploads to PyPI - Package available at https://pypi.org/project/graphforge/
You never manually publish to PyPI - GitHub Actions does it.
When PyPI Gets Updated
Section titled “When PyPI Gets Updated”PyPI is updated only when you create a GitHub Release:
# This DOES trigger PyPI publish ✅gh release create v0.2.0 --title "GraphForge v0.2.0"
# These DO NOT trigger PyPI publish ❌git push origin main # Just a commitgit tag v0.2.0 # Just a taggh pr merge 123 # Just merging a PRVersion on PyPI vs Main Branch
Section titled “Version on PyPI vs Main Branch”It’s normal and expected for these to differ:
PyPI: 0.1.1 ← Latest release (stable)main branch: 0.1.1 ← Version in code (same until bump)[Unreleased]: n/a ← Changes accumulated but not releasedAfter several PRs merge:
PyPI: 0.1.1 ← Still latest releasemain branch: 0.1.1 ← Still same version (not bumped yet)[Unreleased]: → ← 5 changes ready for release → Feature A → Bug fix B → Bug fix C → Improvement D → Docs update EWhen you release 0.2.0:
PyPI: 0.2.0 ← New release publishedmain branch: 0.2.0 ← Version bumped[Unreleased]: (empty) ← Moved to [0.2.0]Labels for Release Tracking
Section titled “Labels for Release Tracking”Use these labels on PRs to track what needs releasing:
Release Impact Labels
Section titled “Release Impact Labels”release:major- Breaking change, requires major version bumprelease:minor- New feature, requires minor version bumprelease:patch- Bug fix, requires patch version bumprelease:none- No release needed (docs, CI, internal)
Release Status Labels
Section titled “Release Status Labels”release:pending- Merged to main, not yet releasedrelease:included- Included in a release
Example Workflow
Section titled “Example Workflow”# 1. PR is createdgh pr create --label "release:minor"
# 2. PR is merged# GitHub Action automatically adds "release:pending"
# 3. When release is created# GitHub Action automatically:# - Removes "release:pending"# - Adds "release:included"# - Adds milestone "v0.2.0"Checking What’s Ready to Release
Section titled “Checking What’s Ready to Release”View Unreleased Changes
Section titled “View Unreleased Changes”# Check CHANGELOG [Unreleased] sectioncat CHANGELOG.md | sed -n '/## \[Unreleased\]/,/## \[/p'Count Unreleased PRs
Section titled “Count Unreleased PRs”# List PRs merged but not releasedgh pr list --state merged --label "release:pending"Compare with Last Release
Section titled “Compare with Last Release”# See all commits since last releasegit log v0.1.1..HEAD --oneline
# See diff since last releasegit diff v0.1.1..HEAD --statRelease Decision Examples
Section titled “Release Decision Examples”Example 1: Accumulate Patches
Section titled “Example 1: Accumulate Patches”Scenario:
- Monday: a prior pull request fixes small bug (merged)
- Tuesday: a prior pull request updates docs (merged)
- Wednesday: a prior pull request fixes another small bug (merged)
Decision: ❌ Don’t release yet
- Only 3 small changes
- No critical bugs
- No major features
- → Keep accumulating
Action:
# All PRs merged to main# CHANGELOG [Unreleased] updated# No version bump, no releaseExample 2: Feature Complete
Section titled “Example 2: Feature Complete”Scenario:
- Week 1-3: Multiple PRs working on OPTIONAL MATCH
- Week 3: Final PR completes OPTIONAL MATCH feature (merged)
- [Unreleased] section has 12 changes
Decision: ✅ Release now
- Major feature complete
- Enough changes accumulated (12 items)
- Good milestone for users
Action:
python scripts/bump_version.py minor # 0.1.1 → 0.2.0# Update CHANGELOGgit commit -am "chore(release): bump version to 0.2.0"git push origin maingit tag -a v0.2.0 -m "Release version 0.2.0"git push origin v0.2.0gh release create v0.2.0 --title "GraphForge v0.2.0 - OPTIONAL MATCH Support"Example 3: Critical Bug
Section titled “Example 3: Critical Bug”Scenario:
- Friday 3pm: User reports data loss bug in production
- Friday 4pm: Bug fix a prior pull request merged
Decision: ✅ Release immediately (hotfix)
- Critical bug affecting users
- Can’t wait for more changes
Action:
python scripts/bump_version.py patch # 0.2.0 → 0.2.1# Update CHANGELOG with hotfixgit commit -am "chore(release): bump version to 0.2.1 (hotfix)"git push origin maingit tag -a v0.2.1 -m "Hotfix release 0.2.1"git push origin v0.2.1gh release create v0.2.1 --title "GraphForge v0.2.1 (Hotfix)"Example 4: Scheduled Release
Section titled “Example 4: Scheduled Release”Scenario:
- Last release: 3 weeks ago (0.2.0)
- [Unreleased] has 8 changes (mix of features and fixes)
- No critical issues
Decision: ✅ Release on schedule
- Scheduled release window (3-4 weeks)
- Enough changes to justify release
- Provides value to users
Action:
# Determine version bump based on changes# If any features: minor, else: patchpython scripts/bump_version.py minor # 0.2.0 → 0.3.0
# Follow normal release processAutomation Opportunities
Section titled “Automation Opportunities”GitHub Actions for Release Tracking
Section titled “GitHub Actions for Release Tracking”Create .github/workflows/release-tracking.yaml:
name: Release Tracking
on: pull_request: types: [closed]
jobs: label-for-release: if: github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - name: Add release:pending label if: contains(github.event.pull_request.labels.*.name, 'release:patch') || contains(github.event.pull_request.labels.*.name, 'release:minor') || contains(github.event.pull_request.labels.*.name, 'release:major') run: | gh pr edit ${{ github.event.pull_request.number }} \ --add-label "release:pending" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Release Reminder Script
Section titled “Release Reminder Script”Create scripts/check_release_needed.py:
#!/usr/bin/env python3"""Check if a release is needed based on unreleased changes."""
import reimport sysfrom datetime import datetime, timedeltafrom pathlib import Path
def count_unreleased_changes(changelog_path: Path) -> int: """Count items in [Unreleased] section.""" content = changelog_path.read_text()
# Extract [Unreleased] section match = re.search( r'## \[Unreleased\](.*?)## \[', content, re.DOTALL )
if not match: return 0
unreleased = match.group(1)
# Count bullet points changes = re.findall(r'^- ', unreleased, re.MULTILINE) return len(changes)
def main(): changelog = Path("CHANGELOG.md")
if not changelog.exists(): print("❌ CHANGELOG.md not found") sys.exit(1)
change_count = count_unreleased_changes(changelog)
print(f"📦 Unreleased changes: {change_count}")
if change_count == 0: print("✅ No unreleased changes. Nothing to release.") elif change_count < 5: print(f"⏳ {change_count} changes accumulated. Consider releasing when you have 5-10 changes.") elif change_count < 10: print(f"⚠️ {change_count} changes ready. Good time to consider a release!") else: print(f"🚨 {change_count} changes accumulated! Time to release.")
sys.exit(0)
if __name__ == "__main__": main()Usage:
# Check if release is neededpython scripts/check_release_needed.pyBest Practices
Section titled “Best Practices”- Merge PRs frequently (continuous integration)
- Update CHANGELOG [Unreleased] with every PR
- Use release labels on PRs
- Batch fixes and features for meaningful releases
- Release every 3-4 weeks on schedule
- Hotfix critical bugs immediately
- Communicate release plans to contributors
DON’T ❌
Section titled “DON’T ❌”- Release after every single PR
- Leave main in broken state
- Forget to update CHANGELOG
- Release without testing
- Skip documentation updates
- Create releases on Fridays (avoid weekend issues)
- Batch security fixes (release immediately)
Communication
Section titled “Communication”Before Release
Section titled “Before Release”Announce intent in GitHub Discussions:
# Preparing v0.3.0 Release
Planning to release v0.3.0 this week (estimated Friday).
## What's included- OPTIONAL MATCH support- Variable-length paths- 8 bug fixes
## Testing needed- Please test branch `main` before release- Report any critical issues
## ETAFriday, March 1stAfter Release
Section titled “After Release”Announce in GitHub Discussions:
# GraphForge v0.3.0 Released! 🎉
We're excited to announce GraphForge v0.3.0!
## Highlights- OPTIONAL MATCH for left outer joins- Variable-length path queries [*1..3]- 15% improvement in query performance
## Installation```bashpip install --upgrade graphforgeFull Changelog
Section titled “Full Changelog”See https://github.com/CurateLabs/graphforge-legecy/releases/tag/v0.3.0
---
## Quick Reference
```bash# Check what's unreleasedcat CHANGELOG.md | grep -A 20 "## \[Unreleased\]"
# Count unreleased changespython scripts/check_release_needed.py
# See changes since last releasegit log $(git describe --tags --abbrev=0)..HEAD --oneline
# List PRs not yet releasedgh pr list --state merged --label "release:pending"
# When ready to releasepython scripts/bump_version.py minor# Edit CHANGELOG, commit, tag, releaseRemember: Main branch ≠ PyPI release. It’s normal for main to be ahead of PyPI. Release when it makes sense, not after every merge.