ci(release): add PyPI Trusted Publishing workflow + /release skill (#358)

GitHub had no publish path — the package was previously released from the
(now-archived) GitLab mirror. Bring publishing to the public repo:

- .github/workflows/release.yml: on a vX.Y.Z tag, build + smoke-test
  (make package) and upload to PyPI via Trusted Publishing (OIDC, no stored
  token), gated behind the `release` environment for manual approval. A guard
  step refuses to publish when the tag != pyproject version.
- .claude/skills/release: /release documents the cut (bump version →
  CHANGELOG → tag → approve → verify) and the one-time PyPI trusted-publisher
  + GitHub environment setup.

Requires two owner-only one-time steps before the first release (documented
in the workflow header and the skill): register the GitHub trusted publisher
on PyPI, and create the `release` environment with required reviewers.

Co-authored-by: zhanghui <zhanghui@shanda.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhanghui 2026-07-28 21:17:17 +08:00 committed by GitHub
parent 42629dfd4d
commit bebb448c89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 109 additions and 1 deletions

View File

@ -0,0 +1,52 @@
---
name: release
description: Cut a versioned release and publish everos to PyPI via the tag-triggered workflow
---
# /release
Publish a new version of `everos` to PyPI. Publishing is automated: pushing a
`vX.Y.Z` tag triggers [.github/workflows/release.yml](../../../.github/workflows/release.yml),
which builds, smoke-tests, and uploads via PyPI **Trusted Publishing** (OIDC —
no stored token) behind the `release` environment's manual-approval gate.
## Preconditions
- On `main`, up to date, with green CI (the tag builds from `main`'s tree).
- Decide the version per SemVer: patch = fixes, minor = back-compatible
features, major = breaking changes.
## Steps
```
1. Bump the version → pyproject.toml [project] version = "X.Y.Z"
(single source; everos.__version__ reads installed package metadata)
2. Update CHANGELOG.md → move the Unreleased entries under a new
## [X.Y.Z] - <date> heading
3. Commit → git commit -m "chore(release): vX.Y.Z"
4. Open a PR, merge to main after green CI
5. Tag main + push → git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z
6. Approve → the release.yml run pauses on the `release`
environment; a reviewer approves in the Actions run
7. Verify → https://pypi.org/project/everos/X.Y.Z/
```
The tag must equal the `pyproject.toml` version — the workflow refuses to
publish on a mismatch.
## Pre-releases
PEP 440 pre-release tags publish too (PyPI accepts them; `pip install everos`
ignores them unless `--pre`): `vX.Y.ZrcN`, `vX.Y.ZaN`, `vX.Y.ZbN`. Set the same
suffix in `pyproject.toml` version before tagging.
## One-time setup (project owner, not doable from CI)
1. **PyPI trusted publisher** — PyPI → project `everos` → Settings →
Publishing → add: owner `EverMind-AI`, repo `EverOS`, workflow
`release.yml`, environment `release`.
2. **GitHub environment** — repo Settings → Environments → create `release`
with required reviewers, so every publish needs a manual approval.
No PyPI API token is ever stored; the workflow mints a short-lived OIDC token
at publish time.

56
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,56 @@
# Publishes to PyPI via Trusted Publishing (OIDC) — no stored token.
# Fires on a version tag (vX.Y.Z, plus PEP 440 pre-releases vX.Y.ZrcN / aN / bN).
# The `release` environment gates the upload behind manual approval; configure
# required reviewers under Settings → Environments → release.
#
# One-time setup on PyPI (project owner, cannot be done from CI):
# PyPI → project `everos` → Settings → Publishing → add a GitHub trusted
# publisher: owner=EverMind-AI, repo=EverOS, workflow=release.yml,
# environment=release.
name: Release
on:
push:
tags:
# vX.Y.Z plus PEP 440 pre-release/dev suffixes (rc1 / a1 / b1 / .dev1).
- "v[0-9]+.[0-9]+.[0-9]+*"
permissions:
contents: read
jobs:
publish:
name: build + publish to PyPI
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # OIDC token for PyPI Trusted Publishing
contents: read
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Set up Python
run: uv python install 3.12
# Guard: the tag must match the package version, so a mistyped tag can't
# publish the wrong release.
- name: Verify tag matches pyproject version
run: |
tag="${GITHUB_REF_NAME#v}"
pkg="$(grep -m1 -E '^version = ' pyproject.toml | sed -E 's/^version = "(.+)"/\1/')"
if [ "$tag" != "$pkg" ]; then
echo "::error::tag v$tag != pyproject version $pkg"
exit 1
fi
- name: Build + smoke-test the package
run: make package # builds sdist+wheel into dist/ and import-smokes it
- name: Publish to PyPI (Trusted Publishing)
uses: pypa/gh-action-pypi-publish@release/v1

View File

@ -38,7 +38,7 @@ Detailed: [docs/architecture.md](docs/architecture.md).
## Engineering practices
- **Coding rules** auto-loaded from [.claude/rules/](.claude/rules/) (10 rules; the three always-loaded ones cover architecture / code-style / language-policy, the rest are path-scoped and load when Claude Code opens a matching file)
- **Workflows** as slash commands in [.claude/skills/](.claude/skills/) — `/commit`, `/new-branch`, `/pr`
- **Workflows** as slash commands in [.claude/skills/](.claude/skills/) — `/commit`, `/new-branch`, `/pr`, `/release`
- **Project-level decisions** in [docs/](docs/) (low-frequency, human-judgment-required)
- **Language policy**: the project targets a global audience — docs and code are English; CJK appears only in test fixtures and locale-suffixed mirrors. Scanned by `make check-cjk`.
- **Datetime discipline**: never call `datetime.now()` / `time.time()` directly — use `everos.component.utils.datetime`. Enforced by `make check-datetime`.