chore(ci): tag-triggered PyPI publish via OIDC trusted-publisher

Tag-on-push (`v0.1.0` shape) builds the sdist+wheel, smoke-imports the
console-script entry point from a fresh venv to catch packaging
regressions, then uploads to PyPI via trusted publisher OIDC — no
API token in repo secrets.

Includes a tag-vs-pyproject version-match guard: aborts the publish
if the tag doesn't match `pyproject.toml`'s `version`. Cheap defense
against the failure mode where the tag advances but pyproject doesn't,
which silently re-publishes the same wheel under a new tag.

README's "Releasing" section walks through the one-time PyPI trusted-
publisher registration the operator must do once before the first
tagged push.

After this lands and the PyPI registration is complete, the codex tab
in the External Connect modal can switch from
  pip install 'git+https://github.com/Molecule-AI/codex-channel-molecule.git'
to
  pip install codex-channel-molecule

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-05-04 18:37:17 -07:00
parent d6eb78dcee
commit dba5970177
2 changed files with 94 additions and 0 deletions

69
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: Publish to PyPI
# Triggered on tag push (vX.Y.Z). Tag-on-push instead of release-creation
# is the cheaper UX — `git tag v0.1.0 && git push origin v0.1.0` ships
# without leaving the terminal.
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
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
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Verify tag matches pyproject version
run: |
tag="${GITHUB_REF#refs/tags/v}"
pkg=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
if [ "$tag" != "$pkg" ]; then
echo "::error::tag $tag does not match pyproject version $pkg — aborting publish to keep PyPI in sync with git tags"
exit 1
fi
- name: Build sdist + wheel
run: |
python -m pip install --upgrade pip build
python -m build
- name: Smoke-import the built wheel
run: |
python -m venv /tmp/install-test
/tmp/install-test/bin/pip install dist/*.whl
/tmp/install-test/bin/codex-channel-molecule --help
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1

View File

@ -70,6 +70,31 @@ pytest -q
Tests are entirely real-subprocess (no mocking the spawn boundary) so the boot path is covered the same way the daemon runs in production.
## Releasing
Tag-on-push triggers `publish.yml` which builds + publishes to PyPI via OIDC trusted publishing (no API token needed).
```sh
# Bump pyproject.toml `version`, commit, then:
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):
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.
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.
## License
Apache-2.0