From 289c65603e7b495389e281102b29db384db68825 Mon Sep 17 00:00:00 2001 From: infra-runtime-be Date: Fri, 15 May 2026 16:59:26 -0700 Subject: [PATCH] fix(pypi): swap OIDC trusted-publisher for twine + PYPI_TOKEN; port .github -> .gitea MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-2026-05-06 PyPI Trusted-Publisher OIDC is dead for our repos — PyPI only accepts GitHub/GitLab/Google/ActiveState issuers, not Gitea. This PR: 1. Renames .github/workflows/{ci,publish}.yml -> .gitea/workflows/. (Gitea Actions reads .gitea/ exclusively on this repo; the .github/ path was silently dead since the migration — saved memory reference_molecule_core_actions_gitea_only.) 2. Replaces `pypa/gh-action-pypi-publish` (which requires OIDC id-token exchange that PyPI rejects from Gitea) with `python -m twine upload --username __token__ --password "$PYPI_TOKEN"`. Mirrors the canonical pattern in molecule-core/.gitea/workflows/publish-runtime.yml that has been shipping successfully since 2026-05-11. 3. Drops `permissions: id-token: write` (no longer needed without OIDC). 4. Adds `twine check` to the build step (catches metadata regressions before upload). 5. Adds concurrency group to serialize tag-driven publishes. 6. Updates README "Releasing" section to describe the twine+SSOT model and link to the operator-config rotation runbook. The PYPI_TOKEN secret is fanned out to this repo from the operator-host SSOT by /opt/molecule-bootstrap/sync-pypi-token.sh — see operator-config PR#48. It supersedes PR#4 (which only renamed .github -> .gitea without fixing the OIDC issue) and unblocks the pushed v0.1.3 tag. Co-Authored-By: Claude Opus 4.7 (1M context) --- {.github => .gitea}/workflows/ci.yml | 0 {.github => .gitea}/workflows/publish.yml | 50 +++++++++++++++++------ README.md | 16 ++------ 3 files changed, 42 insertions(+), 24 deletions(-) rename {.github => .gitea}/workflows/ci.yml (100%) rename {.github => .gitea}/workflows/publish.yml (50%) diff --git a/.github/workflows/ci.yml b/.gitea/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yml rename to .gitea/workflows/ci.yml diff --git a/.github/workflows/publish.yml b/.gitea/workflows/publish.yml similarity index 50% rename from .github/workflows/publish.yml rename to .gitea/workflows/publish.yml index 6e05a6b..884f12c 100644 --- a/.github/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -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/* diff --git a/README.md b/README.md index 1cf8c97..749095b 100644 --- a/README.md +++ b/README.md @@ -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