AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Python Release

skill-paldom-python-skills-python-release · by Paldom

Cuts and automates releases of a Python package — version bumps, changelogs, git tags, PyPI trusted publishing (OIDC), and the tag-triggered publish workflow with a gated environment. Use for 'cut a release', 'publish to PyPI', 'bump the version', 'automate releases', 'set up trusted publishing'. Not for packaging metadata, build backends, or non-release CI.

No reviews yet
0 installs
38 views
0.0% view→install

Install

$ agentstack add skill-paldom-python-skills-python-release

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-paldom-python-skills-python-release)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Python Release? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

python-release

Take a Python package from "the code is ready" to "the new version is on PyPI" with a repeatable, secured release pipeline: a deliberate versioning strategy, bump + changelog + tag, OIDC trusted publishing, and a tag-triggered GitHub Actions publish workflow with a human gate. This skill exists because release automation fails in specific, repeatable ways: uv.lock desync after a version bump, trusted-publisher exact-match mismatches producing invalid-publisher errors, tags created with GITHUB_TOKEN that never trigger the publish workflow, and commit-scraped changelogs nobody can read.

Commands below use uv/uvx. Without uv, substitute python3 -m build + twine upload for build/publish, edit [project] version by hand, and run pinned tools via pip install == — noted here once.

When NOT to use

  • pyproject.toml metadata, build backend choice, src layout, building or

verifying wheels/sdists — the python-packaging skill, if installed, covers that.

  • Lint/type/test CI quality gates, matrices, required checks, general workflow

hardening — the python-ci skill.

  • Dependabot, pip-audit, secret scanning, SBOMs, CODEOWNERS — the

python-supply-chain skill.

  • Enforcing conventional-commit messages with git hooks — the python-precommit

skill (this skill only consumes the convention).

  • Writing announcement prose or marketing copy for a release — no skill needed.

Workflow

1. Take stock of the repo

Run the audit script first — it detects most of the failure modes this skill fixes:

python3 "${CLAUDE_SKILL_DIR}/scripts/check_release_setup.py" --repo .

Then confirm by hand:

  • Package name in [project]. Check https://pypi.org/project// NOW —

name collisions surface embarrassingly late, at first upload.

  • Where the version lives: static project.version, a duplicated

__init__.py __version__, or dynamic (hatch-vcs / uv-dynamic-versioning).

  • Existing tags and their format (git tag --list 'v*'), existing

CHANGELOG.md, existing publish workflow, and any PYPI_API_TOKEN / UV_PUBLISH_TOKEN secrets to retire.

2. Choose a versioning strategy (deliberately — this is unsettled)

There is no community consensus; pick one on purpose and record it:

| Strategy | How | Choose when | Cost | | --- | --- | --- | --- | | Static (default here) | uv version --bump patch\|minor\|major rewrites project.version and refreshes uv.lock | You want explicit, readable version history in git | Needs a bump commit; can drift from the tag if a bump is forgotten | | Dynamic (git-tag-derived) | hatch-vcs or uv-dynamic-versioning computes the version from the tag at build time | Tag and package version must never diverge; no bump commits wanted | Disables uv version introspection; adds a build-time plugin; opaque to newcomers |

Either way: pyproject.toml (or the tag) is the single source of truth. Do not maintain a hand-written __version__ in __init__.py — read it at runtime:

from importlib.metadata import version
__version__ = version("your-package-name")

Configs for both strategies: [references/versioning-and-changelog.md](references/versioning-and-changelog.md).

3. Choose the automation level (three philosophies)

The Python release-tool landscape is fragmented — there is no npm-style "just install semantic-release" default. Three workable levels:

| Level | Tool | Ship trigger | Best for | | --- | --- | --- | --- | | Human-cut, tag-driven (default here) | uv version + git tag; CI publishes on the tag | Maintainer pushes a v* tag | Most packages; no commit-discipline dependency | | PR-gated automation | release-please — maintains a standing Release PR with version + changelog | Human merges the Release PR | Teams wanting automation with a review gate | | Fully automated | python-semantic-release (PSR) from conventional commits | Every qualifying push to main | Strict conventional-commits discipline, high release cadence |

Default to human-cut unless the user asks for more automation: it is the only level with zero dependence on commit-message discipline, and every source on agentic/automated releases converges on automating the mechanical steps while keeping a human on "should this ship". Full PSR and release-please workflow YAML: [references/publish-workflows.md](references/publish-workflows.md).

4. Choose a changelog strategy

  • Default: Keep a Changelog. Maintain CHANGELOG.md with an

## [Unreleased] section (subheadings from the spec vocabulary: Added / Changed / Deprecated / Removed / Fixed / Security). At release time, rename it to ## [X.Y.Z] - YYYY-MM-DD and start a fresh Unreleased section.

  • Commit-scraped (PSR default, or git-cliff): zero effort, but

practitioners widely deride raw commit dumps as "technically correct and completely useless" for end users. If chosen, either post-process into user-facing prose before publishing, or accept the trade-off knowingly.

  • Never let tooling append dates or any extra text into the **version string

itself** — 2.1.0 (2026-01-07) breaks strict semver parsers downstream. Dates belong in the heading, outside the version.

5. Register the trusted publisher on PyPI

OIDC trusted publishing replaces long-lived API tokens: PyPI mints a short-lived (~15 min), package-scoped token for the exact workflow identity — nothing stored, nothing to rotate or exfiltrate. Configure it BEFORE the first workflow run:

  1. New (never-published) project:

→ "Add a new pending publisher". Existing project: project page → Settings → Publishing.

  1. Fill the four fields — validation is an exact, case-sensitive match:
  • Owner (GitHub org/user) and repository name, exact casing.
  • Workflow filename — the bare filename with extension, e.g. publish.yml

(not a path, not publish.yaml if the file is .yml).

  • Environment name: pypi (recommended; must then match environment: in

the job).

  1. In the repo: Settings → Environments → pypi → add required reviewers.

This is the human release gate — the publish job pauses for approval. (Environment protection rules require a public repo or a paid plan.)

  1. After the first successful OIDC publish, revoke any leftover project-scoped

API tokens — a compromised account can otherwise mint a token and publish around the OIDC flow entirely.

Full mechanics, limitations, and threat model: [references/trusted-publishing.md](references/trusted-publishing.md).

6. Write the publish workflow

Tag-triggered, split into test → build → publish so the job holding the OIDC permission never checks out or executes project code:

# .github/workflows/publish.yml — filename must match the trusted-publisher entry
name: Publish

on:
  push:
    tags: ["v*.*.*"]

permissions: {} # deny-all default; each job requests only what it needs

jobs:
  test:
    # A tag push does NOT inherit the tagged commit's CI status — re-run the
    # quality gate here (or reuse your CI via workflow_call) before publishing.
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
      - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
      - run: uv sync --locked # fails loudly if uv.lock desynced from pyproject.toml
      - run: uv run pytest

  build:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
      - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
      - run: uv build
      - run: uvx --from twine==6.2.0 twine check --strict dist/*
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: dist
          path: dist/

  publish:
    needs: build
    runs-on: ubuntu-latest # GitHub-hosted only; OIDC fails on self-hosted runners
    environment: pypi # the gated environment — must match the PyPI publisher entry
    permissions:
      id-token: write # the only write permission publishing needs
    steps:
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: dist
          path: dist/
      - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
        with:
          skip-existing: true # re-runs after partial failure become idempotent

Notes on this shape:

  • pypa/gh-action-pypi-publish generates PEP 740 attestations automatically

under trusted publishing — no extra flags. (uv publish also auto-detects the OIDC token if you prefer one tool; see the reference for that variant.)

  • Every action is pinned to a full commit SHA with the tag in a comment.

Mutable tags get hijacked (tj-actions, March 2025) — and the publish workflow is the highest-value target in the repo. Refresh a pin with: gh api repos///commits/ --jq .sha.

  • Keep the publish step in this directly-triggered file. Moving it behind

workflow_call indirection breaks the trusted-publisher exact match — PyPI validates against the top-level workflow's filename.

  • Do not add contents: write here; publishing needs none. If you also create

GitHub Releases, do it in a separate job with its own minimal permission.

Re-run python3 "${CLAUDE_SKILL_DIR}/scripts/check_release_setup.py" — it verifies trigger, permissions, environment, SHA pinning, and leftover token secrets.

7. Cut a release (the repeatable manual flow)

uv version --bump minor          # rewrites pyproject.toml AND refreshes uv.lock
VERSION="$(uv version --short)"

# Roll CHANGELOG.md's Unreleased section into "## [$VERSION] - $(date +%F)"

git add pyproject.toml uv.lock CHANGELOG.md
git commit -m "chore(release): v${VERSION}"
git tag -a "v${VERSION}" -m "v${VERSION}"
git push origin main "v${VERSION}"

Then approve the pypi environment gate when the workflow pauses, and verify.

If anything other than uv version performed the bump (PSR, bump-my-version, a hand edit), refresh and commit the lockfile in the same commit:

uv lock --upgrade-package ""
git add uv.lock

For the first release ever, rehearse against TestPyPI (isolated index, its own trusted-publisher entry) before production — recipe in [references/publish-workflows.md](references/publish-workflows.md).

8. Verify the release

  • Workflow run green; approval step recorded who released.
  • Package page live on PyPI with the new version; file view shows attestations.
  • Install from a clean environment:

uvx --isolated --from "==${VERSION}" python -c "import ".

  • python3 "${CLAUDE_SKILL_DIR}/scripts/check_release_setup.py" reports no errors (tag, version,

and lockfile all agree).

Output spec — what done looks like

  • A recorded versioning decision: static project.version (default) or a

configured dynamic-versioning plugin — never both, never a stray __version__.

  • CHANGELOG.md with the released section and a fresh Unreleased skeleton.
  • A trusted publisher registered on PyPI matching the workflow exactly; no

long-lived PyPI token secrets left in the repo.

  • .github/workflows/publish.yml — tag-triggered, deny-all default

permissions, id-token: write only on the publish job, gated pypi environment, all actions SHA-pinned, tests gating the upload.

  • An annotated vX.Y.Z tag on a commit where pyproject.toml, uv.lock, and

the tag agree on the version.

  • A green publish run and the version installable from PyPI.

Failure modes & gotchas

  • uv.lock desync (the #1 automated-release breaker). A bump tool rewrites

project.version; uv.lock still records the old version; the next uv sync/uv run/uv build fails its freshness check. Fix: run uv lock --upgrade-package between bump and build and commit the lockfile with the bump commit — regenerating it ephemerally in the runner leaves every clone broken. For PSR, put it in build_command.

  • **invalid-publisher / "unable to authenticate" despite correct-looking

config.** One field differs from reality: owner/repo casing, workflow filename (including .yml vs .yaml), or environment name — or the publish step runs behind workflow_call/reusable-workflow indirection, or on a self-hosted runner. Checklist in [references/trusted-publishing.md](references/trusted-publishing.md).

  • Tag pushed by automation never triggers the publish workflow. Resources

created with the default GITHUB_TOKEN do not trigger other workflows — a GitHub platform rule, not a bug in release-please/PSR. Use a fine-grained PAT or a GitHub App token for the tag-creating step.

  • PSR computes no bump or the wrong bump. Shallow checkout — PSR needs

fetch-depth: 0 on actions/checkout. Add concurrency: release (with cancel-in-progress: false) so racing pushes don't double-release.

  • Release hangs at the tagging step. Git is configured to sign tags

(semantic-release issue #3065). Disable tag signing in CI.

  • Partial failure leaves a dead tag. semantic-release/PSR tag and create

the GitHub Release before uploading and do not roll back if the upload fails. skip-existing: true makes re-runs idempotent; if atomicity matters, order the pipeline compute-version → publish → tag last.

  • Silent non-bumps under commit-driven automation. Non-conforming commit

messages simply produce no release (semantic-release issue #3642, closed not-planned). For 0.x packages, set PSR's allow_zero_version = true; the JS semantic-release declares 0.x semantics out of scope entirely.

  • uv version --bump side effects. It syncs the environment as a side

effect and cannot update files beyond pyproject.toml (uv issues #15286, #13827) — another reason to drop hand-written __version__ attributes.

  • Multi-index uv publish footguns. uv publish cannot find an index

configured with explicit = true (uv issue #9919); the index url (used by --check-url to detect already-uploaded files) is distinct from publish_url. Also, uv ≥ 0.11.22 uploads wheels before sdists — don't assume sdist-first ordering.

  • Branch protection blocks the release bot's bump commit. Give a dedicated

GitHub App a targeted ruleset bypass; do not hand the bot admin rights, and protect tags with a ruleset so a released vX.Y.Z cannot be re-pointed.

  • Trusted publishing relocates risk rather than removing it. The CI runner

is the new trust boundary: a step injected before publish ships malware with valid attestations. Minimal permissions + SHA pinning (above) are part of the release design; repo-wide workflow hardening belongs to the python-ci and python-supply-chain skills.

Bundled resources

  • [references/trusted-publishing.md](references/trusted-publishing.md) — OIDC

mechanics, exact-match troubleshooting, PEP 740 attestations, limitations, threat model.

  • [references/versioning-and-changelog.md](references/versioning-and-changelog.md)

— static/dynamic configs, tool landscape (PSR, release-please, git-cliff, bump-my-version, …), PSR + uv config, changelog strategies, SemVer caveats.

  • [references/publish-workflows.md](references/publish-workflows.md) — complete

workflow variants: PSR push-to-main, release-please, TestPyPI rehearsal, uv publish instead of the PyPA action.

  • scripts/check_release_setup.py — read-only audit of version/lockfile/tag

agreement and publish-workflow hygiene; non-zero exit on errors.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.