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