diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 447f3e4..e6bc1e2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,5 +17,5 @@ Keep PRs focused — one logical change per PR. - [ ] `ruff format .` applied - [ ] `mypy src` passes - [ ] `pytest` passes (new behavior is covered by tests) -- [ ] `CHANGELOG.md` updated for user-facing changes +- [ ] PR title / commits follow [Conventional Commits](https://www.conventionalcommits.org/) (drives versioning & changelog) - [ ] Docs / docstrings updated if the public API changed diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d0f215..71e5bb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,22 +1,54 @@ name: Release -# Publishes to PyPI when a GitHub Release is published. Uses PyPI Trusted -# Publishing (OIDC) — no API tokens or secrets are stored in the repo. -# One-time setup: add a trusted publisher on PyPI for this repo + the -# `release.yml` workflow + the `pypi` environment. -# https://docs.pypi.org/trusted-publishers/ +# Automated releases via release-please + PyPI Trusted Publishing. +# +# Flow: +# 1. Pushes to main run release-please, which maintains a "release PR" that +# bumps the version (src/senderkit/_version.py) and CHANGELOG.md from the +# Conventional Commit history. +# 2. Merging that release PR tags the version + creates the GitHub Release and, +# in the same run, builds and publishes to PyPI. +# +# Everything stays in ONE workflow run so it does not depend on the GitHub +# Release event (events raised by GITHUB_TOKEN do not trigger other workflows). +# +# One-time repo setup: +# - Settings → Actions → General → Workflow permissions: +# enable "Allow GitHub Actions to create and approve pull requests". +# - A PyPI Trusted Publisher for this repo → workflow "release.yml" → +# environment "pypi". https://docs.pypi.org/trusted-publishers/ on: - release: - types: [published] + push: + branches: [main] permissions: contents: read jobs: + release-please: + name: Release please + runs-on: ubuntu-latest + permissions: + contents: write # create tags + GitHub releases + pull-requests: write # open/update the release PR + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + build: name: Build distributions + needs: release-please + if: needs.release-please.outputs.release_created == 'true' runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 @@ -28,9 +60,9 @@ jobs: run: python -m build - name: Check metadata run: twine check dist/* - - name: Verify version matches release tag + - name: Verify built version matches the release tag env: - TAG: ${{ github.event.release.tag_name }} + TAG: ${{ needs.release-please.outputs.tag_name }} run: | VERSION="$(python -c 'import senderkit; print(senderkit.__version__)')" echo "Package version: $VERSION" @@ -46,12 +78,14 @@ jobs: publish: name: Publish to PyPI - needs: build + needs: [release-please, build] + if: needs.release-please.outputs.release_created == 'true' runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/senderkit permissions: + contents: read id-token: write # required for Trusted Publishing steps: - uses: actions/download-artifact@v8 diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..e18ee07 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.0" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06d8009..2c52555 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,17 +46,28 @@ pytest --cov=senderkit `mypy`-clean. - **Style.** `ruff` handles linting and formatting (100-char lines). Match the surrounding code; the CI format gate is enforced. -- **Changelog.** Add an entry to `CHANGELOG.md` for any user-facing change. -- **Commits & PRs.** Keep PRs focused on one logical change. Write clear commit - messages explaining the *why*. +- **Commits & PRs.** Keep PRs focused on one logical change. Use + [Conventional Commit](https://www.conventionalcommits.org/) messages — the + version bump and changelog are derived from them, so the prefix matters: + - `fix:` → patch release, `feat:` → minor release. + - `feat!:` or a `BREAKING CHANGE:` footer → breaking change. + - `docs:`, `chore:`, `test:`, `refactor:`, `ci:` → no release on their own. + - On squash-merge GitHub uses the **PR title**, so keep it conventional too. +- **Changelog.** Generated automatically from commit messages — don't edit + `CHANGELOG.md` by hand. ## Releasing (maintainers) -1. Bump `VERSION` in `src/senderkit/_version.py` (single source of truth) and - update `CHANGELOG.md`. -2. Merge to `main`. -3. Create a GitHub Release with tag `vX.Y.Z` matching the new version. -4. The `Release` workflow builds and publishes to PyPI via Trusted Publishing. +Releases are automated with +[release-please](https://github.com/googleapis/release-please): + +1. Merge PRs to `main` with Conventional Commit messages. +2. release-please maintains a **release PR** that bumps `VERSION` in + `src/senderkit/_version.py` (the single source of truth) and updates + `CHANGELOG.md`. Review and edit it as needed. +3. Merging the release PR tags `vX.Y.Z`, creates the GitHub Release, and the + `Release` workflow builds and publishes to PyPI via Trusted Publishing — all + in one run. ## Code of Conduct diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..d2ec1c7 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "simple", + "package-name": "senderkit", + "changelog-path": "CHANGELOG.md", + "extra-files": ["src/senderkit/_version.py"] + } + } +} diff --git a/src/senderkit/_version.py b/src/senderkit/_version.py index af196f9..778398f 100644 --- a/src/senderkit/_version.py +++ b/src/senderkit/_version.py @@ -1,3 +1,3 @@ """Single source of truth for the SDK version, surfaced in the User-Agent header.""" -VERSION = "0.1.0" +VERSION = "0.1.0" # x-release-please-version