ci: classic auth via group-scoped TWINE_USERNAME/TWINE_PASSWORD, v0.1.4

Trusted Publishing OIDC is blocked for git.unturf.com — PyPI's GitLab
provider hardcodes the issuer to gitlab.com (no self-hosted instance
field in the Add publisher form). Until PyPI lights up self-hosted
GitLab support, classic API token auth is our path.

Auth source: TWINE_USERNAME=__token__ + TWINE_PASSWORD=<pypi-...> set
as group-level CI variables on python/. Both python/ago and
python/erldistpy inherit them automatically; engineering/ group gets
the same vars for make-post-sell + remarkbox.

What this commit changes:
  - .gitlab-ci.yml pypi-twine stage drops the id_tokens block we
    briefly tried (OIDC), uses latest twine (no <6 pin) — twine 6
    reads TWINE_USERNAME/TWINE_PASSWORD env vars and prefers them
    over the OIDC attempt
  - pyproject.toml build-system requires drops the setuptools<77
    cap (latest setuptools emits Metadata-Version 2.4 which twine 6
    reads fine; locally verified)
  - docs/PYPI-TRUSTED-PUBLISHING.md updated to note the issuer-
    hardcoded blocker and the recipe to migrate later when it
    unblocks (or if we mirror to gitlab.com)
This commit is contained in:
russell@unturf.com 2026-06-16 15:01:23 -04:00
parent 049666b061
commit dee08976ef
No known key found for this signature in database
2 changed files with 25 additions and 18 deletions

View file

@ -25,22 +25,20 @@ test:
# ---------------------------------------------------------------------------
# Ship to PyPI on tag pushes.
#
# Tag the commit (``git tag -a v0.0.1 -m 'release'`` then
# ``git push --tags``) and CI builds sdist + wheel and uploads with
# twine. Credentials come from GitLab CI variables ``TWINE_USERNAME``
# (typically ``__token__``) and ``TWINE_PASSWORD`` (the PyPI API token).
# Auth: TWINE_USERNAME + TWINE_PASSWORD env vars come from project-scoped
# GitLab CI variables (Settings → CI/CD → Variables, masked + protected).
# Standard values: TWINE_USERNAME=__token__ and TWINE_PASSWORD=<pypi-...>.
#
# Trusted Publishing OIDC would be cleaner but PyPI's GitLab provider is
# hardcoded to gitlab.com — git.unturf.com self-hosted isn't supported.
# See docs/PYPI-TRUSTED-PUBLISHING.md for the migration recipe whenever
# PyPI adds custom-issuer support (or whenever we mirror to gitlab.com).
# ---------------------------------------------------------------------------
pypi-twine:
stage: pypi-twine
tags: ["build"]
only:
- tags
# GitLab mints a short-lived OIDC ID token (audience=pypi) and injects
# it as PYPI_ID_TOKEN. Twine v6+ auto-detects it and exchanges it with
# PyPI for a scoped upload token. No long-lived secret on the runner.
id_tokens:
PYPI_ID_TOKEN:
aud: pypi
script:
- python3 -m venv .venv
- . .venv/bin/activate

View file

@ -1,15 +1,24 @@
# Migrating to PyPI Trusted Publishing (OIDC)
Today's auth path: classic API token in `/home/gitlab-runner/.pypirc`
on `build.unturf.com`, used by twine 5.x. Works, but every project that
ships from this runner shares one token, rotation is manual, and we're
pinning `twine<6` + `setuptools<77` to keep the legacy fallback alive.
**Status: BLOCKED for self-hosted git.unturf.com**
Trusted Publishing replaces that with short-lived OIDC tokens that
GitLab mints per-job and PyPI exchanges for an upload-only API token.
No long-lived secret on the runner. Per-project, audited per-pipeline.
PyPI's GitLab Trusted Publisher provider has the issuer URL **hardcoded
to `https://gitlab.com`**. There's no "Issuer URL" / "GitLab instance"
field in the "Add publisher" form. Until PyPI ships support for custom
GitLab issuers (or we mirror releases to gitlab.com), Trusted
Publishing is off the table for our python/* repos.
## When to migrate
Today's working auth path: project-scoped GitLab CI variables
``TWINE_USERNAME=__token__`` + ``TWINE_PASSWORD=<pypi-...>`` (masked +
protected). Twine 6 reads them as env vars and skips the OIDC attempt.
Per-project tokens; can be rotated independently.
When PyPI lights up self-hosted GitLab support (track:
https://github.com/pypi/warehouse/issues — search "self-hosted gitlab
trusted publisher"), or if we move to gitlab.com, the recipe below
applies.
## When to migrate (once unblocked)
Single coordinated change across all four python/* repos. Each repo
needs its PyPI pending publisher set up *before* its CI YAML switches.