|
| 1 | +# Automated release system |
| 2 | + |
| 3 | +This document explains how pyjanitor's automated release system works |
| 4 | +and the design decisions behind it. |
| 5 | +Understanding this system helps maintainers know |
| 6 | +when releases happen and how to control versioning. |
| 7 | + |
| 8 | +## Release philosophy |
| 9 | + |
| 10 | +pyjanitor follows a continuous delivery approach to releases: |
| 11 | + |
| 12 | +- **Patch releases** happen automatically on every merge to `dev` |
| 13 | +- **Minor and major releases** are triggered manually by maintainers |
| 14 | + |
| 15 | +The rationale is straightforward: |
| 16 | +most merges are bug fixes, documentation improvements, |
| 17 | +or small enhancements that don't require human decision-making |
| 18 | +about version numbers. |
| 19 | +By automating patch releases, |
| 20 | +we reduce the friction of getting changes into users' hands |
| 21 | +while preserving human control for significant version bumps. |
| 22 | + |
| 23 | +## How automatic releases work |
| 24 | + |
| 25 | +When code is merged to the `dev` branch, the following sequence occurs: |
| 26 | + |
| 27 | +1. **Tests run first**: The `pyjanitor tests` workflow executes, |
| 28 | + running the full test suite across Python 3.11, 3.12, and 3.13. |
| 29 | + |
| 30 | +2. **Release triggers on success**: If all tests pass, |
| 31 | + the `Auto-release` workflow automatically triggers |
| 32 | + via GitHub's `workflow_run` event. |
| 33 | + |
| 34 | +3. **Duplicate check**: The workflow checks |
| 35 | + if the current commit already has a release tag. |
| 36 | + This prevents re-releasing |
| 37 | + when the workflow's own version bump commits trigger another test run. |
| 38 | + |
| 39 | +4. **Version bump**: If no tag exists, |
| 40 | + `bump2version` increments the patch version |
| 41 | + (e.g., 0.32.3 becomes 0.32.4). |
| 42 | + |
| 43 | +5. **Release notes generation**: The `llamabot` CLI generates release notes |
| 44 | + using an LLM, summarizing changes since the last release. |
| 45 | + |
| 46 | +6. **Build and publish**: The package is built and published to PyPI |
| 47 | + using trusted publishing. |
| 48 | + |
| 49 | +7. **GitHub release**: A GitHub release is created |
| 50 | + with the generated release notes. |
| 51 | + |
| 52 | +The entire process is hands-off after merging. |
| 53 | +Maintainers don't need to do anything for patch releases. |
| 54 | + |
| 55 | +## Manual releases for minor and major versions |
| 56 | + |
| 57 | +When a change warrants a minor or major version bump, |
| 58 | +maintainers trigger the release manually: |
| 59 | + |
| 60 | +**When to use minor version (e.g., 0.32.0 to 0.33.0):** |
| 61 | + |
| 62 | +- New features that don't break existing functionality |
| 63 | +- Significant enhancements to existing features |
| 64 | +- New optional dependencies or modules |
| 65 | + |
| 66 | +**When to use major version (e.g., 0.x.x to 1.0.0):** |
| 67 | + |
| 68 | +- Breaking changes to the public API |
| 69 | +- Removal of deprecated features |
| 70 | +- Fundamental changes to how the library works |
| 71 | + |
| 72 | +**How to trigger a manual release:** |
| 73 | + |
| 74 | +1. Go to the [Actions tab][actions] in the repository |
| 75 | +2. Select "Auto-release" from the workflows list |
| 76 | +3. Click "Run workflow" |
| 77 | +4. Choose `major`, `minor`, or `patch` from the dropdown |
| 78 | +5. Click the green "Run workflow" button |
| 79 | + |
| 80 | +[actions]: https://github.com/pyjanitor-devs/pyjanitor/actions |
| 81 | + |
| 82 | +The workflow will run the same steps as an automatic release, |
| 83 | +but with your chosen version bump type. |
| 84 | + |
| 85 | +## Technical implementation |
| 86 | + |
| 87 | +The release system uses GitHub Actions' `workflow_run` trigger, |
| 88 | +which fires when another workflow completes. |
| 89 | +Here's how the triggers work: |
| 90 | + |
| 91 | +```yaml |
| 92 | +on: |
| 93 | + # Automatic: triggers after tests complete on dev |
| 94 | + workflow_run: |
| 95 | + workflows: ["pyjanitor tests"] |
| 96 | + types: |
| 97 | + - completed |
| 98 | + branches: |
| 99 | + - dev |
| 100 | + |
| 101 | + # Manual: maintainer-triggered releases |
| 102 | + workflow_dispatch: |
| 103 | + inputs: |
| 104 | + version_name: |
| 105 | + type: choice |
| 106 | + options: |
| 107 | + - major |
| 108 | + - minor |
| 109 | + - patch |
| 110 | +``` |
| 111 | +
|
| 112 | +The job only runs when appropriate: |
| 113 | +
|
| 114 | +```yaml |
| 115 | +if: | |
| 116 | + github.event_name == 'workflow_dispatch' || |
| 117 | + (github.event.workflow_run.conclusion == 'success' && |
| 118 | + github.event.workflow_run.event == 'push') |
| 119 | +``` |
| 120 | +
|
| 121 | +This condition ensures: |
| 122 | +
|
| 123 | +- Manual triggers always run |
| 124 | +- Automatic triggers only run when tests *passed* (not failed) |
| 125 | +- Automatic triggers only run for *push* events (not PR test runs) |
| 126 | +
|
| 127 | +## Preventing duplicate releases |
| 128 | +
|
| 129 | +When the auto-release workflow runs, |
| 130 | +it creates a version bump commit and pushes it to `dev`. |
| 131 | +This push triggers another test run, |
| 132 | +which could trigger another release. |
| 133 | +To prevent this infinite loop, |
| 134 | +the workflow checks if the current commit already has a release tag: |
| 135 | + |
| 136 | +```yaml |
| 137 | +- name: Check if already released |
| 138 | + id: check_release |
| 139 | + run: | |
| 140 | + CURRENT_COMMIT=$(git rev-parse HEAD) |
| 141 | + if git tag --points-at $CURRENT_COMMIT | grep -q "^v"; then |
| 142 | + echo "already_released=true" >> $GITHUB_OUTPUT |
| 143 | + else |
| 144 | + echo "already_released=false" >> $GITHUB_OUTPUT |
| 145 | + fi |
| 146 | +``` |
| 147 | + |
| 148 | +If a tag exists, all subsequent release steps are skipped. |
| 149 | + |
| 150 | +## Troubleshooting |
| 151 | + |
| 152 | +### Auto-release didn't trigger after merge |
| 153 | + |
| 154 | +Check these common causes: |
| 155 | + |
| 156 | +1. **Tests failed**: The release only triggers on successful test runs. |
| 157 | + Check the Actions tab for test failures. |
| 158 | + |
| 159 | +2. **Not a push event**: Releases only trigger for pushes to `dev`, |
| 160 | + not for pull request test runs. This is intentional. |
| 161 | + |
| 162 | +3. **Already released**: If the commit already has a tag, |
| 163 | + the release is skipped. Check `git tag --points-at HEAD`. |
| 164 | + |
| 165 | +### Release failed mid-way |
| 166 | + |
| 167 | +If a release fails after bumping the version but before publishing: |
| 168 | + |
| 169 | +1. Check the Actions log to identify the failure point |
| 170 | +2. If the tag was created but not pushed, |
| 171 | + you may need to manually push it or delete and recreate |
| 172 | +3. If PyPI publish failed, |
| 173 | + you can re-run the failed job or trigger a manual release |
| 174 | + |
| 175 | +### Wrong version was released |
| 176 | + |
| 177 | +If you need to correct a version: |
| 178 | + |
| 179 | +1. Do not try to overwrite an existing PyPI release |
| 180 | + (PyPI doesn't allow this) |
| 181 | +2. Instead, create a new patch release with the fix |
| 182 | +3. If the version number itself is wrong, |
| 183 | + update `.bumpversion.cfg` and `pyproject.toml` manually, |
| 184 | + commit, and trigger a new release |
| 185 | + |
| 186 | +### How to skip a release for a specific merge |
| 187 | + |
| 188 | +Currently, all merges to `dev` trigger a release. |
| 189 | +If you need to merge something without releasing |
| 190 | +(e.g., CI-only changes), you have two options: |
| 191 | + |
| 192 | +1. Merge to a different branch first, |
| 193 | + then batch multiple changes into a single merge to `dev` |
| 194 | +2. Cancel the auto-release workflow run manually |
| 195 | + in the Actions tab before it completes |
| 196 | + |
| 197 | +**Note:** There's an open issue ([#1549][issue]) |
| 198 | +to consider adding filters for non-code changes. |
| 199 | + |
| 200 | +[issue]: https://github.com/pyjanitor-devs/pyjanitor/issues/1549 |
0 commit comments