diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..412c415 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,83 @@ +name: CD + +on: + push: + tags: + - '*' + +permissions: + contents: read + +jobs: + validate-tag: + name: Validate release tag + runs-on: ubuntu-latest + outputs: + release_tag: ${{ steps.validate.outputs.release_tag }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Validate tag and package version + id: validate + run: | + python - <<'PY' + import os + import re + import tomllib + from pathlib import Path + + tag = os.environ["GITHUB_REF_NAME"] + version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"] + is_release = re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", tag) is not None + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + print(f"release_tag={str(is_release).lower()}", file=output) + if not is_release: + print(f"Tag {tag!r} is not a PyPI release tag; skipping publish.") + raise SystemExit(0) + if version != tag: + raise SystemExit(f"pyproject version {version!r} does not match tag {tag!r}") + print(f"Release version verified: {version}") + PY + + build: + name: Build distribution + needs: validate-tag + if: needs.validate-tag.outputs.release_tag == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + - name: Install build tools + run: python -m pip install build twine + - name: Build package + run: python -m build + - name: Check distribution metadata + run: python -m twine check dist/* + - name: Upload distribution artifact + uses: actions/upload-artifact@v4 + with: + name: python-distribution + path: dist/* + if-no-files-found: error + + publish-pypi: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + contents: read + id-token: write + steps: + - name: Download distribution artifact + uses: actions/download-artifact@v4 + with: + name: python-distribution + path: dist + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md index bc7ce6d..87dc5cd 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,32 @@ To keep agents from falling back to local human credentials, update existing Git - Avoid `@me` assumptions because the GitHub App bot is not the human operator. - Require write summaries to include the returned `auth_mode`, `app_slug`, `installation_id`, repository, operation, and URL/path. +## Releasing to PyPI + +The package is built with Hatchling and publishes through the `CD` GitHub Actions workflow using PyPI Trusted Publishing / OIDC. The workflow listens to all pushed tags but only builds and publishes when the tag matches: + +```text +^[0-9]+\.[0-9]+\.[0-9]+$ +``` + +The tag must also match `project.version` in `pyproject.toml`. + +Before the first release, configure PyPI Trusted Publishing for this repository and workflow: + +- PyPI project name: `hermes-github-app-plugin` +- Owner/repository: this GitHub repository +- Workflow name: `cd.yaml` +- Environment name: `pypi` + +Release example: + +```bash +git tag 0.1.0 +git push origin 0.1.0 +``` + +Tags like `v0.1.0`, `0.1`, or `0.1.0rc1` will not publish. + ## Hermes tools The plugin registers these tools: