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
12 changes: 7 additions & 5 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
---
name: release
description: Release a new md-condition version to PyPI — verify master, then create the GitHub release whose tag sets the version and triggers the publish workflow. Usage: /release <version>
description: "Release a new md-condition version to PyPI — verify master, then create the GitHub release whose tag sets the version and triggers the publish workflow. Usage: /release <version>"
argument-hint: "<version, e.g. 0.2.0>"
disable-model-invocation: true
---

Release version `$ARGUMENTS` (e.g. `0.1.9`, no `v` prefix). If empty, show the latest tag (`git describe --tags --abbrev=0`) and ask which version to release.
Requested version: `$ARGUMENTS`. If empty, show the latest tag (`git describe --tags --abbrev=0`) and ask which version to release.

The version comes from the git tag via setuptools-scm, so there is no file to bump.

0. Normalize the version before using it anywhere: strip whitespace and **one leading `v`/`V`** (`v0.2.0` → `0.2.0`). Call the result VERSION and the tag `vVERSION` — never prefix `v` to an unnormalized argument. VERSION must be a final PEP 440 release, `X.Y.Z` (`^\d+\.\d+\.\d+$`), and greater than the latest tag; if not, stop and ask. Use VERSION / `vVERSION` in place of `$ARGUMENTS` in every step below.
1. Preconditions: on `master`, working tree clean, `git pull` up to date. `git tag` must not already contain `v$ARGUMENTS`. The latest `ci-ok` run on master must be green (`gh run list --branch master --workflow pythonapp.yml --limit 1`). Stop and report if any fail.
2. Verify locally — both must pass:
- `uv run pytest`
- `uv run ruff check .` and `uv run ruff format --check .`
3. Summarize commits since the previous tag (`git log <prev-tag>..HEAD --oneline`) as release notes, and show them to the user for confirmation before creating the release.
4. `gh release create v$ARGUMENTS --target master --title v$ARGUMENTS --notes "<notes>"`. This fires `.github/workflows/pythonpublish.yml`, which builds with version `$ARGUMENTS` and uploads to PyPI.
5. Report the release URL and check the publish run with `gh run list --workflow pythonpublish.yml --limit 1`.
3. Summarize commits since the previous tag (`git log <prev-tag>..HEAD --oneline`) as release notes. If the release contains user-visible behavior changes, add a "Breaking changes / migration" section. Show the notes to the user for confirmation before creating the release.
4. `gh release create v$ARGUMENTS --target master --title v$ARGUMENTS --notes-file <notes>`. Publishing the release fires `.github/workflows/pythonpublish.yml` (`release: published`; a draft does not).
5. The workflow's `build` job re-runs lint + tests and checks the built version equals the tag; then the `publish` job runs in the `pypi` environment, which only accepts `v*` tags and **waits for the user's approval**. Report the release URL and the run (`gh run list --workflow pythonpublish.yml --limit 1`), and tell the user to approve it in the Actions UI. Do not approve it yourself.
8 changes: 4 additions & 4 deletions .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- run: uv run ruff format --check .
- run: uv run ruff check .

Expand All @@ -32,8 +32,8 @@ jobs:
markdown: '3.2.*'
name: test (py${{ matrix.python-version }}${{ matrix.markdown != 'locked' && format(', markdown {0}', matrix.markdown) || '' }})
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
python-version: ${{ matrix.python-version }}
- name: Test
Expand Down
42 changes: 34 additions & 8 deletions .github/workflows/pythonpublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,49 @@ name: Upload Python Package

on:
release:
types: [created]
types: [published]

permissions:
contents: read

jobs:
deploy:
# Test and build without OIDC permissions; only the publish job can mint a PyPI token
build:
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # PyPI Trusted Publishing
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # setuptools-scm derives the version from tags
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Test
run: |
uv run ruff check .
uv run pytest
- name: Build
run: uv build
- name: Check version matches tag
env:
TAG: ${{ github.event.release.tag_name }}
run: |
# A malformed tag (e.g. vv0.2.0) makes setuptools-scm fall back to a dev version
ls dist
test -f "dist/md_condition-${TAG#v}-py3-none-any.whl"
test -f "dist/md_condition-${TAG#v}.tar.gz"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist
path: dist/

publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # PyPI Trusted Publishing
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Release

Create a GitHub release with tag `v<version>` (use `/release`). `.github/workflows/pythonpublish.yml` builds with the tag as the version and uploads to PyPI via Trusted Publishing (OIDC, GitHub environment `pypi`; no API token secrets).
Create a GitHub release with tag `v<version>` (use `/release`). `.github/workflows/pythonpublish.yml` builds with the tag as the version and uploads to PyPI via Trusted Publishing (OIDC, GitHub environment `pypi`; no API token secrets). The `pypi` environment only accepts `v*` tags and requires the maintainer's approval; `v*` tags are protected from deletion and moving by a tag ruleset. `MANIFEST.in` keeps repo tooling (`.claude/`, `.github/`, CLAUDE.md, uv.lock) out of the sdist.
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# setuptools-scm adds every git-tracked file to the sdist; keep repo tooling out
prune .claude
prune .github
exclude .gitignore CLAUDE.md uv.lock
Loading