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. # Ship to PyPI on tag pushes.
# #
# Tag the commit (``git tag -a v0.0.1 -m 'release'`` then # Auth: TWINE_USERNAME + TWINE_PASSWORD env vars come from project-scoped
# ``git push --tags``) and CI builds sdist + wheel and uploads with # GitLab CI variables (Settings → CI/CD → Variables, masked + protected).
# twine. Credentials come from GitLab CI variables ``TWINE_USERNAME`` # Standard values: TWINE_USERNAME=__token__ and TWINE_PASSWORD=<pypi-...>.
# (typically ``__token__``) and ``TWINE_PASSWORD`` (the PyPI API token). #
# 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: pypi-twine:
stage: pypi-twine stage: pypi-twine
tags: ["build"] tags: ["build"]
only: only:
- tags - 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: script:
- python3 -m venv .venv - python3 -m venv .venv
- . .venv/bin/activate - . .venv/bin/activate

View file

@ -1,15 +1,24 @@
# Migrating to PyPI Trusted Publishing (OIDC) # Migrating to PyPI Trusted Publishing (OIDC)
Today's auth path: classic API token in `/home/gitlab-runner/.pypirc` **Status: BLOCKED for self-hosted git.unturf.com**
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.
Trusted Publishing replaces that with short-lived OIDC tokens that PyPI's GitLab Trusted Publisher provider has the issuer URL **hardcoded
GitLab mints per-job and PyPI exchanges for an upload-only API token. to `https://gitlab.com`**. There's no "Issuer URL" / "GitLab instance"
No long-lived secret on the runner. Per-project, audited per-pipeline. 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 Single coordinated change across all four python/* repos. Each repo
needs its PyPI pending publisher set up *before* its CI YAML switches. needs its PyPI pending publisher set up *before* its CI YAML switches.