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
57 changes: 51 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,59 @@ jobs:
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, windows, macos, sdist]
environment:
name: pypi
url: https://pypi.org/p/common-expression-language
permissions:
# id-token: write mints the OIDC token PyPI trusted publishing exchanges
# for a short-lived upload token, so no PyPI API token is stored here.
id-token: write
# contents: write lets the last step create the GitHub release for the tag.
contents: write
steps:
- uses: actions/download-artifact@v5
- uses: actions/checkout@v5

- name: Collect wheels and sdist
uses: actions/download-artifact@v5
with:
pattern: wheels-*
merge-multiple: true
path: dist

- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
command: upload
args: --non-interactive --skip-existing wheels-*/*
skip-existing: true

- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
echo "Release $GITHUB_REF_NAME already exists; refreshing its assets."
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
# `gh release create` with assets creates a draft, uploads, then publishes,
# and deletes the draft if an upload fails. A job that dies before that
# cleanup runs leaves a draft behind, so publish it rather than exiting
# with the release invisible.
if [ "$(gh release view "$GITHUB_REF_NAME" --json isDraft --jq .isDraft)" = "true" ]; then
echo "Release was left as a draft by an earlier run; publishing it."
gh release edit "$GITHUB_REF_NAME" --draft=false
fi
exit 0
Comment thread
hardbyte marked this conversation as resolved.
fi
version="${GITHUB_REF_NAME#v}"
awk -v v="$version" '
$0 ~ "^## \\[" v "\\]" { found = 1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > notes.md
if [ -s notes.md ]; then
gh release create "$GITHUB_REF_NAME" --verify-tag \
--title "$GITHUB_REF_NAME" --notes-file notes.md dist/*
else
echo "::warning::No CHANGELOG section for $version; using generated notes."
gh release create "$GITHUB_REF_NAME" --verify-tag \
--title "$GITHUB_REF_NAME" --generate-notes dist/*
fi
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Updated

- Releases publish to PyPI with
[trusted publishing](https://docs.pypi.org/trusted-publishers/) instead of a
stored `PYPI_API_TOKEN`: the release job exchanges a short-lived GitHub OIDC
token for the upload credential, so no long-lived PyPI token is kept in repository
secrets. The job runs in a `pypi` environment, which the trusted publisher on
PyPI is configured against.
- The release job now also creates the GitHub release for the pushed tag, using the
matching `CHANGELOG.md` section as the release notes and attaching the wheels and
sdist. Pushing a tag previously published to PyPI but left no GitHub release
behind.

## [0.8.0] - 2026-08-19

Adds the `sum` aggregation to the extended standard library, refreshes the locked
Expand Down
25 changes: 22 additions & 3 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,26 @@ uv run pytest --profile tests/test_performance.py

## Release Process

1. **Version Bump** - Update version in `pyproject.toml`
2. **Changelog** - Document changes in `CHANGELOG.md`
3. **Release** - Create a release in GitHub to trigger publishing to PyPI
1. **Version bump** - update `version` in `Cargo.toml`. `pyproject.toml` takes its
version from there via maturin, so `Cargo.toml` is the single source; run
`cargo check` afterwards so `Cargo.lock` picks up the new version.
2. **Changelog** - turn the `Unreleased` section of `CHANGELOG.md` into a dated
`## [X.Y.Z] - YYYY-MM-DD` section. The release job uses that section verbatim as
the GitHub release notes, so it is worth writing well.
3. **Tag** - merge those changes, then tag the merge commit and push the tag:

```bash
git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z
```

Pushing the tag runs the full test matrix, builds wheels for every supported
platform plus the sdist, uploads them to PyPI, and creates the GitHub release for
the tag with the changelog section as its notes and the built artifacts attached.
Pushing the tag is the only manual step - there is no "draft a release" click.

PyPI uploads use [trusted publishing](https://docs.pypi.org/trusted-publishers/):
the `release` job mints a short-lived OIDC token from GitHub rather than using a
stored API token. The publisher is registered on PyPI against this repository, the
`ci.yml` workflow and the `pypi` environment, so renaming the workflow file or the
environment means updating the trusted publisher on PyPI as well.

Loading