Skip to main content

Python package build and release

This guide covers building edox-ops distributions and publishing them to:

RegistryPurpose
GitLab PyPIProject package registry (first-class internal mirror)
TestPyPIPre-release validation before public PyPI
PyPIPublic Python Package Index

Publishing uses standard python -m build and twine. Credentials are never committed; use environment variables or GitLab CI/CD variables.

Installing from PyPI (end users)

Operator install steps (venv, version pins, host notes) are in the published Installation guide. Quick reference after a release is on the public index:

pip install edox-ops
edox-ops --version

Package page: pypi.org/project/edox-ops.

Versioning

  • Source of truth: src/edox_ops/__version__ (also exposed as edox_ops.__version__).
  • pyproject.toml reads the version dynamically from that attribute.
  • Git tags: use v<version>, e.g. v0.1.0 for package version 0.1.0.
  • CI on tags checks that the tag matches the built package version.

Bump __version__ in a commit before tagging:

git tag -a v0.1.0 -m "Release 0.1.0"
git push gitlab v0.1.0

Local setup

python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev,release]"

The release extra installs build and twine.

Build and validate

make dist-clean # optional: remove dist/, build/, egg-info
make build # python -m build → dist/*.tar.gz, dist/*.whl
make dist-check # twine check dist/*

Or without make:

python -m build
python -m twine check dist/*

Artifacts land in dist/ (gitignored).

Credentials (local)

Copy .pypirc.example to ~/.pypirc and fill tokens locally, or export variables (preferred for CI parity):

VariableUsed for
TWINE_USERNAMEUsually __token__ for PyPI and TestPyPI
TWINE_PASSWORDPyPI API token (upload scope)
TWINE_PASSWORD_TESTPYPITestPyPI API token (if different from PyPI)
GITLAB_PYPI_PROJECT_IDNumeric GitLab project ID (local GitLab uploads)
GITLAB_TOKENGitLab PAT with write_package_registry (local GitLab uploads)

PyPI and TestPyPI tokens: create at pypi.org and test.pypi.org.

Never commit .pypirc with real passwords (the repo ignores .pypirc).

TestPyPI

export TWINE_USERNAME=__token__
export TWINE_PASSWORD="$TWINE_PASSWORD_TESTPYPI" # or TWINE_PASSWORD
make publish-testpypi

Test installation (TestPyPI does not mirror all dependencies):

pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
edox-ops==0.1.0
edox-ops --help

Public PyPI

export TWINE_USERNAME=__token__
export TWINE_PASSWORD="$TWINE_PASSWORD_PYPI"
make publish-pypi

GitLab PyPI registry

CI (recommended): push a version tag; job publish:gitlab uses CI_JOB_TOKEN.

Local:

export GITLAB_PYPI_PROJECT_ID=12345678 # Project → Settings → General
export GITLAB_TOKEN=glpat-... # scope: write_package_registry
make publish-gitlab

Registry URL pattern:

https://gitlab.com/api/v4/projects/<project_id>/packages/pypi

On self-managed GitLab, replace the host with your instance.

GitLab CI release pipeline

On git tags (v*), after lint, test, and integration:

JobStageBehavior
package:buildbuildpython -m build, twine check, artifact dist/
publish:gitlabpublishUpload to GitLab PyPI (automatic on tag)
package:smokereleaseInstall dist/*.whl in a fresh venv; CLI smoke
publish:testpypipublishUpload to TestPyPI (automatic after package:smoke)
package:smoke-testpypipublishpip install from TestPyPI
publish:pypipublishUpload to PyPI (manual)
package:smoke-pypipublishpip install from PyPI

Required CI/CD variables

Configure in Settings → CI/CD → Variables (masked, protected):

VariableJobNotes
TWINE_PASSWORD_TESTPYPIpublish:testpypiTestPyPI API token
TWINE_PASSWORD_PYPIpublish:pypiPyPI API token

publish:gitlab uses built-in CI_JOB_TOKEN (no extra secret).

Optional: set variables protected so only protected tags/branches can use them.

Follow the Automated release pipeline — the canonical plan for all maintainers. Summary:

  1. Bump __version__ and merge to master.
  2. Tag vX.Y.Z and push the tag.
  3. CI runs package:buildpackage:smokepublish:gitlab + publish:testpypipackage:smoke-testpypi.
  4. Click publish:pypi when TestPyPI smoke is green (only manual step).
  5. CI runs package:smoke-pypi after public upload.

Local dry run before tagging: make dist-smoke.

Repo hygiene

Automated tag pipeline (canonical maintainer plan): release-pipeline.md. Branch protection, Dependabot, and first-tag setup: release-hygiene.md. Package history: CHANGELOG.md.

Troubleshooting

  • Tag / version mismatch: CI fails in package:build if v1.2.3__version__.
  • Duplicate upload: PyPI and TestPyPI reject re-uploading the same version; bump version or yank on the index.
  • TestPyPI install fails on dependencies: use --extra-index-url https://pypi.org/simple/ as above.