skills/pypi-ops/SKILL.md
Publish Python packages to PyPI via OIDC Trusted Publishing (PEP 740 attestations, gh-action-pypi-publish) instead of stored tokens. Use for: invalid-publisher errors, pending-publisher 404s, uv publish/twine, TestPyPI, environment approval gates, and rotating/auditing publish tokens.
npx skillsauth add 0xDarkMatter/claude-mods pypi-opsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Publish Python packages to PyPI on the 2026 best-practice path: OIDC Trusted
Publishing with signed PEP 740 attestations, no long-lived token to leak. This
skill owns the publish layer (the registry handshake, the first-publish
gotchas, the recovery playbook). General GitHub Actions syntax is ci-cd-ops;
the install-side worm defense is supply-chain-defense; gh/release-page
mechanics are github-ops.
A release spans several skills; pypi-ops owns the registry step. Chain them:
supply-chain-defense
(cooldown + behavioural scan). The build runs dependency code before it
touches your publish credential, so a poisoned build dep can steal the token.scripts/publish-preflight.sh --build . (this skill).git-ops (its push-gate scans for secrets / forbidden
files before the tag goes up).assets/publish.yml; you approve at
the pypi environment gate.github-ops, human-reviewed notes.Default to OIDC Trusted Publishing. Reach for a token only when OIDC is impossible (publishing from a non-supported CI, or a one-off local push).
| | Trusted Publishing (OIDC) ← default | API token |
|---|---|---|
| Secret stored | None — short-lived OIDC token minted per run | Long-lived pypi-… token in a secret |
| Leak/phish blast radius | None to steal | Full publish rights until rotated |
| Provenance | PEP 740 attestations (signed, verifiable) | None by default |
| Setup | One-time publisher registration on PyPI | Generate token + store secret |
| Best for | All CI/CD releases | Legacy CI, emergency local upload |
If a repo currently uses a token, migrating to OIDC is strictly an upgrade — see references/trusted-publishing.md.
A Trusted Publisher is normally configured under the project's settings on PyPI — but on the first ever publish the project doesn't exist yet, so there's nothing to configure it under. The fix is a pending publisher, registered at the account level before the first upload.
Symptom (the exact failure this skill exists to kill):
Trusted publishing exchange failure:
* invalid-publisher: valid token, but no corresponding publisher
(Publisher with matching claims was not found)
The OIDC token was valid; PyPI just has no publisher matching the claims. Fix:
PyPI → https://pypi.org/manage/account/publishing/ → Add a pending publisher
| Field | Value | |---|---| | PyPI Project Name | the dist name from
pyproject.toml[project].name| | Owner | GitHub org/user | | Repository name | repo name | | Workflow name | the filename, e.g.publish.yml(not thename:) | | Environment name | must equal the job'senvironment:(e.g.pypi) |
All four claims must match the run's OIDC token exactly. After the first
successful publish, the pending publisher auto-converts to a normal project
publisher — no further action. Run diagnose-publish.sh on a failed run to read
the exact claims it presented and compare them field-by-field.
This is the most common silent-failure mode: a package's
publish.ymllooks perfect and every release builds green, yet nothing ever reaches PyPI because the publisher was never registered. Check it first.
The shipped template is hardened to the patterns below — adapt the marked points
and drop it in .github/workflows/. Non-negotiables it encodes:
on: push: tags: ['v*'] — release on a version tag, never on every push.publish job has permissions: id-token: write and
pypa/gh-action-pypi-publish with attestations: true. No password:/token.environment: pypi on the publish job → a human approves every release
(defense-in-depth: even a compromised repo can't auto-ship).build job (no elevated perms) produces + uploads
the dist artifact; publish downloads it. Least privilege per job.uv sync --locked + pip-audit: the release is built against the
committed, hash-verified lockfile and blocked if a dep has a known CVE.twine check / metadata validation before upload.# vX comment (mutable tags get
hijacked — see check-action-pins.py).Stealing your publish credential lets an attacker ship malware to everyone who installs you — so the publish path is the surface the 2026 worm campaign (Mini Shai-Hulud) targets, minting PyPI/npm tokens from stale OIDC trust and orphaned workflows. The template above isn't just convention; each choice is a defense:
| Control | Defends against |
|---|---|
| OIDC, no stored token | Credential theft/phishing — there is no long-lived secret to steal |
| PEP 740 attestations | Tampered artifacts — provenance is signed and verifiable |
| environment: pypi + reviewers | A compromised repo/CI auto-shipping — a human still gates the release |
| pip-audit gate | A knowingly-vulnerable dependency reaching the release build |
| SHA-pinned actions (check-action-pins.py) | Action-tag hijacks (tj-actions, 2025) repointing @vN to a malicious commit |
| permissions: {} + per-job least privilege | A poisoned build step escalating beyond read |
| uv sync --locked | Build-time dependency injection / silent re-resolution |
Then audit the trust itself, not just the workflow:
uv sync
step runs before your OIDC token is even minted.Division of labour: pypi-ops owns publisher hardening; supply-chain-defense
owns the install side and ships integrity-audit.sh (hunts pull_request_target
Before tagging, run the preflight so a release never fails on something mechanical (version skew, dirty lock, missing publisher config):
scripts/publish-preflight.sh . # human summary; exit 10 = not ready
scripts/publish-preflight.sh --build . # also build + twine-check the dist
scripts/publish-preflight.sh --json . | jq '.data[] | select(.ok==false)'
It checks: pyproject version == __init__.__version__, the version is not
already on PyPI (uploads are immutable — you cannot re-push 1.2.3), the
lockfile self-version matches, a tag (if present) matches the version, and the
publish workflow uses OIDC (flags a stored token). --build additionally
verifies the package actually builds and passes twine check. Dynamic-versioned
projects (hatch-vcs / setuptools-scm) are read from the HEAD tag. Green → bump,
commit, tag, push the tag; CI builds, waits at the pypi environment gate, you
approve.
scripts/diagnose-publish.sh <run-id> # reads gh run log, names the cause + fix
gh run view <run-id> --log-failed | scripts/diagnose-publish.sh - # or pipe a log
The high-frequency failure classes and their fixes:
| Symptom | Cause | Fix |
|---|---|---|
| invalid-publisher / claims not found | No (pending) publisher on PyPI | Register the pending publisher (above) |
| File already exists / 400 on upload | Version already on PyPI (immutable) | Bump the version; never reuse — see recovery |
| Job stuck "Waiting" | environment: pypi needs approval | Approve the deployment in the run's UI |
| environment … not found | Publisher claim names an env the job lacks | Make environment: and the publisher's Environment match |
| Built green, not on PyPI | Silent accept / no verify step | Add the verify-on-PyPI job; re-run |
| non-OIDC/token rejected | Token wrong/expired, or OIDC misread as token | Prefer OIDC; if token, rotate + re-store |
Full catalogue with the underlying mechanics: references/recovery-playbook.md.
For a one-off or a non-CI environment. Prefer uv in 2026 (faster, native):
uv build # sdist + wheel into dist/
uv publish --trusted-publishing automatic # OIDC if in supported CI, else prompts
# token path (store in ~/.pypirc or env, never inline on the CLI history):
UV_PUBLISH_TOKEN="pypi-…" uv publish
twine remains the canonical fallback and the metadata validator (the GitHub
Action wraps it internally):
python -m twine check dist/* # ALWAYS run before any upload
python -m twine upload dist/* # token from ~/.pypirc; legacy path
Never hand-roll the HTTP upload. Details + ~/.pypirc shape:
references/uv-publish.md.
For a brand-new package or a risky metadata change, publish to test.pypi.org
first — it has its own separate accounts and its own pending-publisher
registration. Point the action at repository-url: https://test.pypi.org/legacy/
and register the pending publisher on TestPyPI. See
references/trusted-publishing.md.
The pinned action SHAs and pypa/gh-action-pypi-publish major drift over time.
The verifier flags it before a release does:
scripts/check-action-pins.py --offline .github/workflows/publish.yml # structure: all pinned + commented
scripts/check-action-pins.py --live .github/workflows/publish.yml # resolve tags → flag SHA drift
--offline is the PR gate (every uses: is SHA-pinned with a # vX comment);
--live runs scheduled (resolves each pin against GitHub and exits 10 on drift,
7 if GitHub is unreachable — advisory, never a flaky block).
When several repos publish the same way, don't copy publish.yml N times — each
copy drifts its own SHA pins. Hoist the publish job into a reusable workflow
(on: workflow_call) in one repo, and have each package's tiny caller pass its
dist name. OIDC still works: the caller's workflow_ref is what PyPI matches,
so register each package's pending publisher against the caller filename
(e.g. release.yml), not the shared one. One place to refresh pins
(check-action-pins.py on the reusable workflow); one approval gate definition;
per-package publishers. See references/trusted-publishing.md
for the claim that must match.
| File | Load when |
|---|---|
| references/trusted-publishing.md | Setting up OIDC, pending vs project publisher, OIDC claim semantics, environments, TestPyPI, token→OIDC migration |
| references/recovery-playbook.md | A publish failed and you need the full failure-class catalogue + mechanics |
| references/uv-publish.md | Local/manual publishing, uv build/uv publish, twine, ~/.pypirc, build backends |
testing
Audit any repo against the agentic-quality doctrine — score entry docs, structure, and enforcement gates, then map each finding to its fix. Triggers on: repo doctor, repo audit, agentic quality, is this repo agent-friendly, doc drift, stale AGENTS.md, monorepo structure, nested CLAUDE.md.
data-ai
Router for parallel or recurring agent work across six skills. Covers: parallel agents, fan out work, delegate to workers, run overnight, scheduled loop, land branches, mixed-model fleet, orchestrate workers, background agents at scale. Triggers on: which skill for parallel work, fan out agents, spawn workers, run this overnight, schedule a loop, land my branches, heterogeneous fleet, delegate to cheaper model, autonomous loop.
tools
Heterogeneous cross-provider fleet - GLM (z.ai), Codex (OpenAI), Grok (xAI), Anthropic Sonnet/Opus/Haiku - from one session, porting the native Workflow tool's patterns (adversarial verify, judge panels, journal resume) to OS-process workers. Triggers: fleetflow, heterogeneous/mixed-model fleet, codex worker, grok worker, cross-provider fan-out, cross-model verify.
development
Application/game-scale three.js: ES modules, GLTF pipeline (DRACO/KTX2/meshopt), AnimationMixer, physics (rapier/cannon-es), react-three-fiber, and performance at scale (InstancedMesh, LOD, draw calls). Triggers on: three.js, GLTFLoader, r3f, game loop, WebGL memory leak, boids.