diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..6219f0a6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,89 @@ +name: Publish to PyPI + +# Publishes to PyPI via OIDC trusted publishing whenever a v* tag is pushed. +# No API tokens required — PyPI verifies the GitHub OIDC identity instead. +# +# git tag v0.7.16 # must match the version in pyproject.toml +# git push origin v0.7.16 + +on: + push: + tags: + - "v*" + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + permissions: + contents: read # for checkout; upload-artifact uses the Actions runtime token, not GITHUB_TOKEN + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false # don't leave GITHUB_TOKEN in git config; build runs third-party code + # Submodule (spec/) is not needed — cloudglue/sdk is already committed. + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tooling + run: python -m pip install --upgrade build + + - name: Verify tag matches pyproject version + run: | + PKG_VERSION="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')" + TAG_VERSION="${GITHUB_REF_NAME#v}" + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then + echo "::error::Tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match pyproject version $PKG_VERSION" + exit 1 + fi + echo "Building and publishing version $PKG_VERSION" + + - name: Build sdist and wheel + run: python -m build + + - name: Check artifacts + run: | + python -m pip install --upgrade twine + python -m twine check dist/* + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/cloudglue + permissions: + id-token: write # required for OIDC trusted publishing + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - uses: pypa/gh-action-pypi-publish@release/v1 + + github-release: + name: Create GitHub Release + needs: publish # only release after a successful PyPI publish + runs-on: ubuntu-latest + permissions: + contents: write # required to create the release and upload assets + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Create release with sdist + wheel attached + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true diff --git a/README.md b/README.md index 708dce6c..0a3c3bf9 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,52 @@ make generate # Generate SDK from OpenAPI spec make build # Build the package ``` +### Releasing + +Releases are published to PyPI automatically by the +[`Publish to PyPI`](.github/workflows/publish.yml) GitHub Actions workflow +whenever a `v*` tag is pushed. It uses +[OIDC trusted publishing](https://docs.pypi.org/trusted-publishers/) — no API +tokens are stored anywhere. + +To cut a release: + +```bash +# 1. Bump the version in pyproject.toml (e.g. 0.7.15 -> 0.7.16) and commit it. +# 2. Tag the commit. The tag MUST match the pyproject.toml version, prefixed +# with "v" — the workflow fails fast if they differ. +git tag v0.7.16 +git push origin v0.7.16 +``` + +Pushing the tag triggers, in order: + +1. **build** — verifies the tag matches `pyproject.toml`, builds the sdist + + wheel, runs `twine check`. +2. **publish** — uploads the artifacts to PyPI via OIDC. +3. **github-release** — creates a [GitHub Release](https://github.com/cloudglue/cloudglue-python/releases) + for the tag with auto-generated notes and the `.tar.gz` + `.whl` attached. + +#### One-time setup + +This is already configured for the `cloudglue` project, but for reference the +workflow depends on: + +- A [PyPI trusted publisher](https://pypi.org/manage/project/cloudglue/settings/publishing/) + for repo `cloudglue/cloudglue-python`, workflow `publish.yml`, environment `pypi`. +- A GitHub Environment named `pypi` (repo **Settings → Environments**). + +#### Manual publishing (fallback) + +The legacy token-based path still works if you need to publish outside CI. It +requires a [PyPI API token](https://pypi.org/manage/account/token/) scoped to the +`cloudglue` project, in `~/.pypirc` or via env vars: + +```bash +make build +TWINE_USERNAME=__token__ TWINE_PASSWORD='pypi-...' make publish +``` + ### Project Structure Project directory structure described below: