---
title: "feat: Automate PyPi publishing with OIDC, no-prefix tags, and commit-based changelog"
type: feat
status: completed
date: 2026-03-02
origin: docs/brainstorms/2026-03-02-pypi-publishing-brainstorm.md
---

# feat: Automate PyPi publishing with OIDC, no-prefix tags, and commit-based changelog

## Overview
Automate the build, versioning, and publishing lifecycle for the `condensr` package. This plan implements a modern, secure, and DRY (Don't Repeat Yourself) release workflow using GitLab CI, PyPi Trusted Publishing (OIDC), and `setuptools-scm`. The versioning will use the `X.Y.Z` tag format (no `v` prefix), and the changelog will be generated from commit messages using `commitizen`.

## Problem Statement / Motivation
Currently, publishing is a manual process that requires sensitive API tokens and manual version updates in `pyproject.toml`. This is prone to error, risks credential leakage, and creates "version bump" commit noise. The user specifically wants to move to a tag-driven approach where the changelog reflects the actual commit messages between releases.

## Proposed Solution
1.  **Tag-Driven Versioning (No Prefix)**: Use `setuptools-scm` to derive the package version from Git tags in the format `X.Y.Z`.
2.  **Secret-less Publishing**: Use OIDC (Trusted Publisher) to authenticate GitLab CI with PyPi.
3.  **Commit-Based Changelog**: Use `commitizen` to parse conventional commit messages and update `CHANGELOG.md` automatically.
4.  **Integrated Pipeline**: A single GitLab CI pipeline that tests, builds, and publishes on tag push.

## Technical Considerations
- **Tag Format**: `setuptools-scm` will be configured to handle tags without the `v` prefix (e.g., `0.1.0`).
- **Changelog Flow**: `commitizen` will be used locally or in CI to update the changelog based on the difference between the last tag and the current state.
- **Environment Isolation**: A GitLab "release" environment will be used to scope the OIDC token.

## System-Wide Impact
- **Interaction Graph**: Commit (using Conventional Commits) -> `cz bump` (updates `CHANGELOG.md` & tags `X.Y.Z`) -> Push Tag -> GitLab CI Trigger -> `setuptools-scm` (detects version) -> `build` -> `twine` (publishes with OIDC).
- **Error Propagation**: If `cz bump` fails, the release process halts before the tag is created, ensuring a clean history.
- **State Lifecycle Risks**: If the `CHANGELOG.md` update fails to commit, the tag might point to an old changelog state.

## Acceptance Criteria
- [ ] `pyproject.toml` uses `setuptools-scm` for versioning with `X.Y.Z` compatibility.
- [ ] `commitizen` is configured in `pyproject.toml` to update `CHANGELOG.md`.
- [ ] `condensr/__init__.py` (or CLI) accesses the version via `importlib.metadata`.
- [ ] GitLab CI has a `publish` stage that runs only on tags matching `^[0-9]+\.[0-9]+\.[0-9]+$`.
- [ ] `publish` job uses `id_tokens` with `aud: pypi` for OIDC authentication.
- [ ] A successful `cz bump && git push --tags` results in a new PyPi release with an updated changelog.

## Implementation Phases

### Phase 1: Build & Versioning Configuration
1.  Modify `pyproject.toml` to:
    - Add `setuptools-scm[simple]` to `build-system.requires`.
    - Set `project.dynamic = ["version"]`.
    - Add `[tool.commitizen]` configuration for changelog and tag format.
2.  Update `condensr/__init__.py` to use `importlib.metadata.version("condensr")`.

### Phase 2: GitLab CI Configuration
1.  Update `.gitlab-ci.yml`:
    - Define a `release` environment.
    - Add `id_tokens` for `PYPI_ID_TOKEN`.
    - Implement `build` and `publish` jobs.
    - Ensure tags are filtered correctly (no `v` prefix).

### Phase 3: External Configuration (Manual Steps)
1.  Configure PyPi Trusted Publisher for the GitLab repository.
2.  Create and protect the `release` environment in GitLab.

## Sources & References
- **Origin brainstorm:** [docs/brainstorms/2026-03-02-pypi-publishing-brainstorm.md](docs/brainstorms/2026-03-02-pypi-publishing-brainstorm.md)
- **Tooling**: [setuptools-scm](https://setuptools-scm.readthedocs.io/), [commitizen](https://commitizen-tools.github.io/commitizen/).

## MVP Implementation Example (Pseudo-code)

### pyproject.toml
```toml
[build-system]
requires = ["setuptools>=80", "setuptools-scm[simple]>=8"]
build-backend = "setuptools.build_meta"

[project]
name = "condensr"
dynamic = ["version"]

[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "$version"  # No 'v' prefix
version_scheme = "semver"
update_changelog_on_bump = true
```

### .gitlab-ci.yml
```yaml
publish-pypi:
  stage: publish
  image: python:3.12
  environment: release
  id_tokens:
    PYPI_ID_TOKEN:
      aud: pypi
  script:
    - pip install build twine
    - python -m build
    - TWINE_USERNAME=__token__ TWINE_PASSWORD=$PYPI_ID_TOKEN twine upload dist/*
  rules:
    - if: $CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+$/
```
