Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 69 additions & 26 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@ jobs:
echo "Manual dispatch; version comes from setuptools-scm."
fi

- name: Verify release notes exist
id: notes
# Checked here, before the build, rather than left to action-gh-release: that action
# fails on a missing body_path only after everything has been built, signed, and
# uploaded, which reads as a late and unrelated failure. A rehearsal (workflow_dispatch)
# has no rel- tag and so no notes to look for, hence the push-only guard.
# Every release from 1.16.0 on ships notes; see CLAUDE.md "Cutting a release".
if: github.event_name == 'push'
run: |
set -euo pipefail
NOTES="doc/release-notes/${GITHUB_REF_NAME}.md"
if [[ ! -f "$NOTES" ]]; then
echo "::error::Release notes not found at $NOTES."
echo "::error::Add the notes for ${{ steps.version.outputs.version }} and retag once they are on the tagged commit."
exit 1
fi
echo "notes=$NOTES" >> "$GITHUB_OUTPUT"

- name: Build wheel and sdist
run: |
python -m pip install --upgrade pip build twine
Expand Down Expand Up @@ -126,6 +144,49 @@ jobs:
with:
inputs: ./dist/*.whl ./dist/*.tar.gz ./dist/SHA256SUMS

- name: Assemble the release body
# The publish job downloads artifacts but never checks out the repo, so the notes
# have to travel with the dist/ upload. They are concatenated with the verification
# and install instructions here rather than passed to action-gh-release as `body`,
# because `body_path` takes precedence over `body` outright (it is a fallback, not a
# companion) -- setting both would silently drop these instructions from the release.
# action-gh-release appends its generated commit list after whatever body it resolves,
# so generate_release_notes still composes on top of this.
if: github.event_name == 'push'
env:
NOTES: ${{ steps.notes.outputs.notes }}
run: |
set -euo pipefail
{
cat "$NOTES"
cat <<'BODY'

## Verifying these artifacts

Checksums:

```bash
sha256sum -c SHA256SUMS
```

Signatures (keyless Sigstore; `pip install sigstore`):

```bash
sigstore verify identity \
--cert-identity "https://github.com/${{ github.repository }}/.github/workflows/release.yml@${{ github.ref }}" \
--cert-oidc-issuer "https://token.actions.githubusercontent.com" \
dp_python_lib-*.whl
```

## Installing

```bash
pip install dp_python_lib-*.whl
```
BODY
} > dist/RELEASE_BODY.md
cat dist/RELEASE_BODY.md

- name: Upload build outputs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down Expand Up @@ -163,29 +224,11 @@ jobs:
dist/*.sigstore.json
generate_release_notes: true
fail_on_unmatched_files: true
body: |
## Verifying these artifacts

Checksums:

```bash
sha256sum -c SHA256SUMS
```

Signatures (keyless Sigstore; `pip install sigstore`):

```bash
sigstore verify identity \
--cert-identity "https://github.com/${{ github.repository }}/.github/workflows/release.yml@${{ github.ref }}" \
--cert-oidc-issuer "https://token.actions.githubusercontent.com" \
dp_python_lib-*.whl
```

## Installing

```bash
pip install dp_python_lib-*.whl
```
# Assembled in the build job: the hand-written doc/release-notes/rel-X.Y.Z.md
# followed by the verification and install instructions. RELEASE_BODY.md is
# deliberately absent from `files` above -- it is the release body, not an
# artifact to download.
body_path: dist/RELEASE_BODY.md

# ---------------------------------------------------------------------------------------
# PyPI publishing -- WIRED UP BUT INTENTIONALLY DISABLED.
Expand Down Expand Up @@ -221,9 +264,9 @@ jobs:
path: dist

- name: Remove non-distribution files
# Only sdists and wheels may be uploaded; the checksums file and Sigstore bundles
# would be rejected.
run: rm -f dist/SHA256SUMS dist/*.sigstore.json
# Only sdists and wheels may be uploaded; the checksums file, the assembled release
# body, and the Sigstore bundles would all be rejected.
run: rm -f dist/SHA256SUMS dist/RELEASE_BODY.md dist/*.sigstore.json

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
Expand Down
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ The tag must be exactly `rel-X.Y.Z` with no suffix — prerelease shapes like
`rel-1.15.0-rc1` are rejected up front, because setuptools-scm would normalize them
(`1.15.0rc1`) and fail the tag-vs-built version assertion with a confusing error.

**Release notes** are version-controlled under `doc/release-notes/`, one document per
release (`rel-X.Y.Z.md`), starting with 1.16.0; the same convention as dp-grpc. Organize
a note by issue ticket rather than by PR, since a ticket often spans several PRs, and lead
a breaking release with an "Upgrading from <previous>" checklist that separates silent
behavior changes from outright errors.

`release.yml` publishes the document as the GitHub release body: the build job concatenates
it with the artifact verification and install instructions into `dist/RELEASE_BODY.md`,
which the publish job passes as `body_path` (that job never checks out the repo, so the
notes travel with the `dist/` upload). Note `body_path` *overrides* `body` rather than
complementing it, so those instructions belong in the assembled file, not in a `body:` input.
GitHub's own generated commit list is appended after all of it.

The build job fails early — before building — if `doc/release-notes/<tag>.md` is missing on
the tagged commit, so **write the notes and merge them before pushing the `rel-*` tag**. A
`workflow_dispatch` rehearsal skips the check, having no tag to derive a filename from.

### Dependencies
Core dependencies are managed in `pyproject.toml`:
- `grpcio` - gRPC runtime
Expand Down