Automated release system
This document explains how pyjanitor's automated release system works and the design decisions behind it. Understanding this system helps maintainers know when releases happen and how to control versioning.
Release philosophy
pyjanitor follows a continuous delivery approach to releases:
- Patch releases happen automatically on every merge to
dev - Minor and major releases are triggered manually by maintainers
The rationale is straightforward: most merges are bug fixes, documentation improvements, or small enhancements that don't require human decision-making about version numbers. By automating patch releases, we reduce the friction of getting changes into users' hands while preserving human control for significant version bumps.
How automatic releases work
When code is merged to the dev branch, the following sequence occurs:
-
Tests run first: The
pyjanitor testsworkflow executes, running the full test suite across Python 3.11, 3.12, and 3.13. -
Release triggers on success: If all tests pass, the
Auto-releaseworkflow automatically triggers via GitHub'sworkflow_runevent. -
Duplicate check: The workflow checks if the current commit already has a release tag. This prevents re-releasing when the workflow's own version bump commits trigger another test run.
-
Version bump: If no tag exists,
bump2versionincrements the patch version (e.g., 0.32.3 becomes 0.32.4). -
Release notes generation: The
llamabotCLI generates release notes using an LLM, summarizing changes since the last release. -
Build and publish: The package is built and published to PyPI using trusted publishing.
-
GitHub release: A GitHub release is created with the generated release notes.
The entire process is hands-off after merging. Maintainers don't need to do anything for patch releases.
Manual releases for minor and major versions
When a change warrants a minor or major version bump, maintainers trigger the release manually:
When to use minor version (e.g., 0.32.0 to 0.33.0):
- New features that don't break existing functionality
- Significant enhancements to existing features
- New optional dependencies or modules
When to use major version (e.g., 0.x.x to 1.0.0):
- Breaking changes to the public API
- Removal of deprecated features
- Fundamental changes to how the library works
How to trigger a manual release:
- Go to the Actions tab in the repository
- Select "Auto-release" from the workflows list
- Click "Run workflow"
- Choose
major,minor, orpatchfrom the dropdown - Click the green "Run workflow" button
The workflow will run the same steps as an automatic release, but with your chosen version bump type.
Technical implementation
The release system uses GitHub Actions' workflow_run trigger,
which fires when another workflow completes.
Here's how the triggers work:
on:
# Automatic: triggers after tests complete on dev
workflow_run:
workflows: ["pyjanitor tests"]
types:
- completed
branches:
- dev
# Manual: maintainer-triggered releases
workflow_dispatch:
inputs:
version_name:
type: choice
options:
- major
- minor
- patch
The job only runs when appropriate:
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push')
This condition ensures:
- Manual triggers always run
- Automatic triggers only run when tests passed (not failed)
- Automatic triggers only run for push events (not PR test runs)
Preventing duplicate releases
When the auto-release workflow runs,
it creates a version bump commit and pushes it to dev.
This push triggers another test run,
which could trigger another release.
To prevent this infinite loop,
the workflow checks if the current commit already has a release tag:
- name: Check if already released
id: check_release
run: |
CURRENT_COMMIT=$(git rev-parse HEAD)
if git tag --points-at $CURRENT_COMMIT | grep -q "^v"; then
echo "already_released=true" >> $GITHUB_OUTPUT
else
echo "already_released=false" >> $GITHUB_OUTPUT
fi
If a tag exists, all subsequent release steps are skipped.
Troubleshooting
Auto-release didn't trigger after merge
Check these common causes:
-
Tests failed: The release only triggers on successful test runs. Check the Actions tab for test failures.
-
Not a push event: Releases only trigger for pushes to
dev, not for pull request test runs. This is intentional. -
Already released: If the commit already has a tag, the release is skipped. Check
git tag --points-at HEAD.
Release failed mid-way
If a release fails after bumping the version but before publishing:
- Check the Actions log to identify the failure point
- If the tag was created but not pushed, you may need to manually push it or delete and recreate
- If PyPI publish failed, you can re-run the failed job or trigger a manual release
Wrong version was released
If you need to correct a version:
- Do not try to overwrite an existing PyPI release (PyPI doesn't allow this)
- Instead, create a new patch release with the fix
- If the version number itself is wrong,
update
.bumpversion.cfgandpyproject.tomlmanually, commit, and trigger a new release
How to skip a release for a specific merge
Currently, all merges to dev trigger a release.
If you need to merge something without releasing
(e.g., CI-only changes), you have two options:
- Merge to a different branch first,
then batch multiple changes into a single merge to
dev - Cancel the auto-release workflow run manually in the Actions tab before it completes
Note: There's an open issue (#1549) to consider adding filters for non-code changes.