Skip to main content

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)

#WhoAction
1MaintainerLand changes on develop, ff-merge to master when CI is green
2MaintainerRun python scripts/bump_release.py (proposes semver from commits; on confirm: updates __version__, finalizes CHANGELOG.md, commits). ff-merge master.
3MaintainerTag and push: git push gitlab vX.Y.Z (or re-run bump script with --tag before push)
4CIRuns the full pipeline below (automatic)
5MaintainerWhen package:smoke-testpypi is green, click publish:pypi (manual)
6CIpackage: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:

SettingWhere
TWINE_PASSWORD_TESTPYPISettings → CI/CD → Variables (masked, protected)
TWINE_PASSWORD_PYPISame (for publish:pypi)
DependabotSettings → Repository → Dependabot + .gitlab/dependabot.yml
Branch protectionmaster (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]
JobStageWhat it proves
ruff, audit:python, audit:website, unit, integration ×3lint–integrationSame quality gate as branch pipelines
package:buildbuildTag matches __version__; python -m build; twine check dist/*
package:smokereleaseFresh venv installs dist/*.whl; edox-ops --version + --help
publish:gitlabpublishUpload to GitLab PyPI registry
publish:testpypipublishUpload to TestPyPI (automatic after wheel smoke)
package:smoke-testpypipublishpip install edox-ops==X.Y.Z from TestPyPI (+ PyPI for deps)
publish:pypipublishUpload to public PyPI (manual — only human gate)
package:smoke-pypipublishpip 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):

ApproachWhen
IncrementalAdd a short bullet under [Unreleased] after meaningful merges on develop
Release-timeSkip [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:

  1. Sets src/edox_ops/__init__.py __version__
  2. Moves [Unreleased] changelog notes into [X.Y.Z] - date
  3. Resets [Unreleased] to the placeholder
  4. Creates chore(release): Bump version to X.Y.Z (with AI: yes when 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

FailureFix
package:smokeFix packaging/metadata; re-run tag pipeline (or delete/recreate tag)
publish:testpypi (auth)Fix TWINE_PASSWORD_TESTPYPI; retry job
package:smoke-testpypiWait/retry (index lag) or fix bad upload; retry job
Bad version already on TestPyPI/PyPIBump __version__, new tag vX.Y.Z+1 — indexes reject re-uploads
publish:pypi not clickedTag 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:buildpages), not from version tags. See pages-domain.md.