fix(pypi): swap OIDC trusted-publisher for twine + PYPI_TOKEN; port .github -> .gitea #6

Merged
devops-engineer merged 1 commits from fix/pypi-gitea-twine-no-oidc into main 2026-05-16 00:18:00 +00:00
3 changed files with 42 additions and 24 deletions

View File

@ -9,15 +9,20 @@ on:
- "v[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
# Post-2026-05-06 (Molecule-AI GitHub org suspension): PyPI's Trusted
# Publisher OIDC flow only accepts GitHub/GitLab/Google/ActiveState
# issuers — not Gitea. This workflow uses a long-lived PyPI API token
# stored as the repo-level secret PYPI_TOKEN, fanned out from the
# operator-host SSOT (/etc/molecule-bootstrap/all-credentials.env) by
# /opt/molecule-bootstrap/sync-pypi-token.sh.
permissions:
contents: read
# OIDC token for PyPI trusted-publisher auth — no secret token needed.
# PyPI side: register
# github.com/Molecule-AI/codex-channel-molecule
# workflow=publish.yml environment=pypi
# under "Trusted publisher management" on the codex-channel-molecule
# PyPI project page (see README "Releasing" section).
id-token: write
# Serialize tag-driven publishes so two concurrent tag pushes don't both
# try to upload the same version and race PyPI.
concurrency:
group: publish-pypi
cancel-in-progress: false
jobs:
build:
@ -40,7 +45,7 @@ jobs:
- name: Build sdist + wheel
run: |
python -m pip install --upgrade pip build
python -m pip install --upgrade pip build twine
python -m build
- name: Smoke-import the built wheel
@ -49,6 +54,9 @@ jobs:
/tmp/install-test/bin/pip install dist/*.whl
/tmp/install-test/bin/codex-channel-molecule --help
- name: Verify package metadata (twine check)
run: python -m twine check dist/*
- uses: actions/upload-artifact@v3 # pinned to v3 for Gitea act_runner v0.6 compatibility (internal#46)
with:
name: dist
@ -57,13 +65,31 @@ jobs:
publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v3 # pinned to v3 for Gitea act_runner v0.6 compatibility (internal#46)
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Publish to PyPI
# PYPI_TOKEN: repo-level Gitea Actions secret, written by
# /opt/molecule-bootstrap/sync-pypi-token.sh from the operator-host
# SSOT. Never set this by hand — rotate via the SSOT instead
# (ops/PYPI_TOKEN_ROTATION.md in operator-config).
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
if [ -z "$PYPI_TOKEN" ]; then
echo "::error::PYPI_TOKEN secret is not set. Run sync-pypi-token.sh on the operator host to fan it out from SSOT."
exit 1
fi
python -m pip install --upgrade twine
python -m twine upload \
--repository pypi \
--username __token__ \
--password "$PYPI_TOKEN" \
dist/*

View File

@ -80,7 +80,7 @@ Tests are entirely real-subprocess (no mocking the spawn boundary) so the boot p
## Releasing
Tag-on-push triggers `publish.yml` which builds + publishes to PyPI via OIDC trusted publishing (no API token needed).
Tag-on-push triggers `.gitea/workflows/publish.yml` which builds + publishes to PyPI via `twine upload` using the `PYPI_TOKEN` repo-level Gitea Actions secret.
```sh
# Bump pyproject.toml `version`, commit, then:
@ -89,19 +89,11 @@ git tag v0.1.1 && git push origin v0.1.1
The workflow refuses to publish if the tag doesn't match `pyproject.toml`'s `version` — keeps PyPI versions and git tags in lockstep.
**One-time PyPI setup** (before the first release):
### Why twine, not Trusted Publisher OIDC
1. Create the project on PyPI by uploading the first wheel manually, OR
2. Pre-register the project on PyPI under a "Pending publisher" config so the first tagged push creates it.
Post-2026-05-06 (Molecule-AI GitHub-org suspension) the canonical SCM is Gitea. PyPI's Trusted-Publisher OIDC flow only recognises GitHub / GitLab / Google / ActiveState issuers — not Gitea — so this repo (and every other PyPI-publishing repo in `molecule-ai/*`) falls back to a long-lived API token.
Either way, on the project's PyPI page → "Manage" → "Publishing" → "Add a new publisher", configure:
- Owner: `Molecule-AI`
- Repository: `codex-channel-molecule`
- Workflow filename: `publish.yml`
- Environment name: `pypi`
After this, every `git push origin v*.*.*` ships the wheel to PyPI without any further intervention.
The `PYPI_TOKEN` secret is **not set by hand**. It is fanned out from the operator-host SSOT (`/etc/molecule-bootstrap/all-credentials.env`) by `/opt/molecule-bootstrap/sync-pypi-token.sh` (see [operator-config/etc/pypi-publishers.yaml](https://git.moleculesai.app/molecule-ai/operator-config/src/branch/main/etc/pypi-publishers.yaml)). Rotation procedure: [PYPI_TOKEN_ROTATION.md](https://git.moleculesai.app/molecule-ai/operator-config/src/branch/main/ops/PYPI_TOKEN_ROTATION.md).
## License