Automated release pipeline
This is the canonical release plan for the edox-ops Python package. Every maintainer
follows the same steps; GitLab CI runs the same tests in the same order on every version tag.
Package credentials and local builds: package-release.md. Repo
settings (Dependabot, branch protection): release-hygiene.md.
Version path to stable 1.0.0: roadmap-1.0.md.
Maintainer actions (only two manual steps)
| # | Who | Action |
|---|---|---|
| 1 | Maintainer | Land changes on develop, ff-merge to master when CI is green |
| 2 | Maintainer | Run python scripts/bump_release.py (proposes semver from commits; on confirm: updates __version__, finalizes CHANGELOG.md, commits). ff-merge master. |
| 3 | Maintainer | Tag and push: git push gitlab vX.Y.Z (or re-run bump script with --tag before push) |
| 4 | CI | Runs the full pipeline below (automatic) |
| 5 | Maintainer | When package:smoke-testpypi is green, click publish:pypi (manual) |
| 6 | CI | package:smoke-pypi verifies the public index install |
Everything except step 5 is automatic once the tag is pushed.
One-time project setup
Before the first tag:
| Setting | Where |
|---|---|
TWINE_PASSWORD_TESTPYPI | Settings → CI/CD → Variables (masked, protected) |
TWINE_PASSWORD_PYPI | Same (for publish:pypi) |
| Dependabot | Settings → Repository → Dependabot + .gitlab/dependabot.yml |
| Branch protection | master (and develop if desired) |
Tag pipeline (automatic)
Triggered by tags matching vX.Y.Z where X.Y.Z equals src/edox_ops/__init__.py
__version__.
flowchart TD
tag[Push tag vX.Y.Z] --> quality[lint + test + integration]
quality --> build[package:build]
build --> smoke_wheel[package:smoke]
smoke_wheel --> gitlab[publish:gitlab]
smoke_wheel --> testpypi[publish:testpypi]
testpypi --> smoke_testpypi[package:smoke-testpypi]
smoke_testpypi --> pypi_manual[publish:pypi manual]
pypi_manual --> smoke_pypi[package:smoke-pypi]
| Job | Stage | What it proves |
|---|---|---|
ruff, audit:python, audit:website, unit, integration ×3 | lint–integration | Same quality gate as branch pipelines |
package:build | build | Tag matches __version__; python -m build; twine check dist/* |
package:smoke | release | Fresh venv installs dist/*.whl; edox-ops --version + --help |
publish:gitlab | publish | Upload to GitLab PyPI registry |
publish:testpypi | publish | Upload to TestPyPI (automatic after wheel smoke) |
package:smoke-testpypi | publish | pip install edox-ops==X.Y.Z from TestPyPI (+ PyPI for deps) |
publish:pypi | publish | Upload to public PyPI (manual — only human gate) |
package:smoke-pypi | publish | pip install edox-ops==X.Y.Z from PyPI |
Script for smoke jobs: scripts/smoke_install_package.py.
Index smoke jobs retry installs (index propagation lag).
Changelog policy
CHANGELOG.md [Unreleased] is a curated summary for operators and package users — not
a dump of commit messages. Git history already holds the full detail; the changelog should
answer “what matters in this release?” in a few grouped bullets (Added, Changed, Fixed,
…).
Two ways to maintain [Unreleased] (pick one):
| Approach | When |
|---|---|
| Incremental | Add a short bullet under [Unreleased] after meaningful merges on develop |
| Release-time | Skip [Unreleased] during development; before tagging, ask a maintainer or Cursor to read git log vX.Y.Z..HEAD, group by theme, and write a curated summary into [Unreleased] (then run bump_release.py) |
Either way, prose is edited for readers — not pasted from commit titles. Do not
use Commitizen-style tools that dump git log into the changelog; that duplicates
git log --oneline without adding readability.
Version sections and dates appear only in the release commit. While developing, keep a
single [Unreleased] block — do not add ## [X.Y.Z] - YYYY-MM-DD or tag footer
links early; that implies a release that has not happened yet. On bump_release.py
confirmation, one commit adds [X.Y.Z] - date (today unless --date), moves [Unreleased]
prose into it, resets [Unreleased], and updates compare/tag links.
bump_release.py uses commits only to suggest semver (feat → minor, fix → patch,
breaking/! → major). It moves whatever prose is already under [Unreleased] into
[X.Y.Z] - date; it does not generate changelog text from commit titles.
Cursor prompt (release-time curation)
Before running bump_release.py, you can ask Cursor to draft [Unreleased] from git
history. Replace v0.1.0 with the latest release tag:
Since v0.1.0, walk commits on develop since that tag and write a curated summary into
CHANGELOG.md under [Unreleased] for the next package release. Group by Added / Changed /
Fixed; short operator-focused bullets, not a commit dump. Do not bump the version yet.
After you review the changelog, run python scripts/bump_release.py (or make bump-release)
to apply the version bump and release commit.
Version bump helper
scripts/bump_release.py proposes the next semver from conventional commits since the
last v* tag. On confirmation it:
- Sets
src/edox_ops/__init__.py__version__ - Moves
[Unreleased]changelog notes into[X.Y.Z] - date - Resets
[Unreleased]to the placeholder - Creates
chore(release): Bump version to X.Y.Z(withAI: yeswhen using the script)
python scripts/bump_release.py # propose + confirm
python scripts/bump_release.py --dry-run # preview only
python scripts/bump_release.py --bump minor # override suggestion
python scripts/bump_release.py --yes --tag # non-interactive + annotated tag
Override with --version 1.2.3 when needed. The script does not push.
Local dry run (before tagging)
Same checks as CI wheel smoke, on your machine:
pip install -e ".[dev,release]"
make build
make dist-check
make dist-smoke
dist-smoke installs dist/*.whl into .smoke-venv and runs the CLI.
Failure recovery
| Failure | Fix |
|---|---|
package:smoke | Fix packaging/metadata; re-run tag pipeline (or delete/recreate tag) |
publish:testpypi (auth) | Fix TWINE_PASSWORD_TESTPYPI; retry job |
package:smoke-testpypi | Wait/retry (index lag) or fix bad upload; retry job |
| Bad version already on TestPyPI/PyPI | Bump __version__, new tag vX.Y.Z+1 — indexes reject re-uploads |
publish:pypi not clicked | Tag pipeline stops after TestPyPI smoke; safe to fix and click later |
See also Troubleshooting in the package release guide.
Documentation deploy (separate from package tags)
Operator docs publish from master / develop branch pipelines (docs:build →
pages), not from version tags. See pages-domain.md.
Related
CHANGELOG.mdgitlab-ci.md— full CI layout