From 28ef120fc6769cd0ac0762c5659e44901c028b34 Mon Sep 17 00:00:00 2001 From: cssho Date: Mon, 26 Oct 2026 21:47:14 +0900 Subject: [PATCH 1/3] Prepare publishing for 0.2.0 - Publish on `release: published` instead of `created`, so drafts don't upload and publishing a draft does - MANIFEST.in: keep .claude/, .github/, CLAUDE.md, uv.lock out of the sdist (setuptools-scm includes every tracked file) - Document the pypi environment approval and tag protection in CLAUDE.md and /release Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012pKTVb5TrGn27R29rHHNck --- .claude/skills/release/SKILL.md | 6 +++--- .github/workflows/pythonpublish.yml | 2 +- CLAUDE.md | 2 +- MANIFEST.in | 4 ++++ 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 MANIFEST.in diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 6c4a726..a0ddc21 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -12,6 +12,6 @@ The version comes from the git tag via setuptools-scm, so there is no file to bu 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 ..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 ""`. 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 ..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 `. Publishing the release fires `.github/workflows/pythonpublish.yml` (`release: published`; a draft does not). +5. 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. diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index aed84e6..027233b 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -2,7 +2,7 @@ name: Upload Python Package on: release: - types: [created] + types: [published] permissions: contents: read diff --git a/CLAUDE.md b/CLAUDE.md index 998ae06..234dd44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` (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` (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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ecd0747 --- /dev/null +++ b/MANIFEST.in @@ -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 From 599ca5503f3c11063bdeb383ac7846003bb45065 Mon Sep 17 00:00:00 2001 From: cssho Date: Mon, 26 Oct 2026 21:50:40 +0900 Subject: [PATCH 2/3] Quote /release skill description (contains ': ', broke YAML frontmatter) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012pKTVb5TrGn27R29rHHNck --- .claude/skills/release/SKILL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index a0ddc21..aaadb94 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -1,6 +1,7 @@ --- 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 +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 " +argument-hint: "" disable-model-invocation: true --- From 73086617431c04bb8d617344dffdcb09a1f92940 Mon Sep 17 00:00:00 2001 From: cssho Date: Mon, 26 Oct 2026 22:03:55 +0900 Subject: [PATCH 3/3] Harden the release pipeline (review findings) - Pin every action to a commit SHA (version in a comment). Also lets Dependabot see setup-uv >= v8, which no longer has floating major tags - Publish workflow: build job (no OIDC) runs lint + tests, builds, and checks the built version equals the release tag; publish job only downloads the artifact and uploads with id-token: write - /release: normalize the version (strip one leading v, require X.Y.Z greater than the latest tag) so `/release v0.2.0` can't tag vv0.2.0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012pKTVb5TrGn27R29rHHNck --- .claude/skills/release/SKILL.md | 5 ++-- .github/workflows/pythonapp.yml | 8 +++--- .github/workflows/pythonpublish.yml | 40 ++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index aaadb94..c46312d 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -5,14 +5,15 @@ argument-hint: "" 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 ..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 `. Publishing the release fires `.github/workflows/pythonpublish.yml` (`release: published`; a draft does not). -5. 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. +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. diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 55ed0e2..430643a 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -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 . @@ -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 diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index 027233b..4124f1f 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -8,17 +8,43 @@ 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