LOCAL-ONLY PyPI publishing with Doppler credential management. Use when publishing to PyPI from LOCAL machine ONLY. NEVER use in CI/CD pipelines. Workspace-wide policy enforces local publishing via scripts/publish-to-pypi.sh with CI detection guards.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
scripts/publish-to-pypi.shThis skill supports LOCAL machine publishing ONLY.
❌ Publishing from GitHub Actions
❌ Publishing from any CI/CD pipeline (GitHub Actions, GitLab CI, Jenkins, CircleCI)
❌ publishCmd in semantic-release configuration
❌ Building packages in CI (uv build in prepareCmd)
❌ Storing PyPI tokens in GitHub secrets
✅ Use scripts/publish-to-pypi.sh on local machine
✅ CI detection guards in publish script
✅ Manual approval before each release
✅ Doppler credential management (no plaintext tokens)
✅ Repository verification (prevents fork abuse)
See: ADR-0027, docs/development/PUBLISHING.md
This skill provides local-only PyPI publishing using Doppler for secure credential management. It integrates with the workspace-wide release workflow where:
| Script | Purpose |
|---|---|
scripts/publish-to-pypi.sh | Local PyPI publishing with CI detection guards |
Usage: Copy to your project's scripts/ directory:
# Environment-agnostic path
PLUGIN_DIR="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/marketplaces/cc-skills/plugins/itp}"
cp "$PLUGIN_DIR/skills/pypi-doppler/scripts/publish-to-pypi.sh" scripts/
chmod +x scripts/publish-to-pypi.sh
Install Doppler CLI:
brew install dopplerhq/cli/doppler
Authenticate with Doppler:
doppler login
Verify access to claude-config project:
doppler whoami
doppler projects
Create PyPI API token:
pypi-AgEIcHlwaS5vcmc..., ~180 characters)Store token in Doppler:
doppler secrets set PYPI_TOKEN='pypi-AgEIcHlwaS5vcmc...' \
--project claude-config \
--config prd
Verify token stored:
doppler secrets get PYPI_TOKEN \
--project claude-config \
--config prd \
--plain
Pre-publish validation: Before publishing to PyPI, verify that the version has incremented from the previous release. Publishing without a version increment is invalid and wastes resources.
Autonomous check sequence:
pyproject.toml version against latest PyPI versionfeat: or fix: types."Why this matters: PyPI rejects duplicate versions, but more importantly, users and package managers rely on version increments to detect updates. A release workflow that doesn't increment version is broken.
Step 1: Development & Commit (Conventional Commits):
# Make your changes
git add .
# Commit with conventional format (determines version bump)
git commit -m "feat: add new feature" # MINOR bump
# Push to main
git push origin main
Step 2: Automated Versioning (GitHub Actions - 40-60s):
GitHub Actions workflow automatically:
@semantic-release/commit-analyzerv7.1.0)pyproject.toml, package.json versionsCHANGELOG.mdv7.1.0)[skip ci] message⚠️ PyPI publishing does NOT happen here (by design - see ADR-0027)
Step 3: Local PyPI Publishing (30 seconds):
After GitHub Actions completes, publish to PyPI locally:
# Pull the latest release commit
git pull origin main
# Publish to PyPI (uses pypi-doppler skill)
./scripts/publish-to-pypi.sh
Expected output:
🚀 Publishing to PyPI (Local Workflow)
======================================
🔐 Step 0: Verifying Doppler credentials...
✅ Doppler token verified
📥 Step 1: Pulling latest release commit...
Current version: v7.1.0
🧹 Step 2: Cleaning old builds...
✅ Cleaned
📦 Step 3: Building package...
✅ Built: dist/gapless_crypto_clickhouse-7.1.0-py3-none-any.whl
📤 Step 4: Publishing to PyPI...
Using PYPI_TOKEN from Doppler
✅ Published to PyPI
🔍 Step 5: Verifying on PyPI...
✅ Verified: https://pypi.org/project/gapless-crypto-clickhouse/7.1.0/
✅ Complete! Published v7.1.0 to PyPI in 28 seconds
CRITICAL: This command must ONLY run on your local machine, NEVER in CI/CD.
# First time: copy script from skill to your project (environment-agnostic)
PLUGIN_DIR="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/marketplaces/cc-skills/plugins/itp}"
cp "$PLUGIN_DIR/skills/pypi-doppler/scripts/publish-to-pypi.sh" scripts/
chmod +x scripts/publish-to-pypi.sh
# After semantic-release creates GitHub release
git pull origin main
# Publish using local copy of bundled script
./scripts/publish-to-pypi.sh
Bundled script features:
For manual publishing without the canonical script:
# Retrieve token from Doppler
PYPI_TOKEN=$(doppler secrets get PYPI_TOKEN \
--project claude-config \
--config prd \
--plain)
# Build package
uv build
# Publish to PyPI
UV_PUBLISH_TOKEN="${PYPI_TOKEN}" uv publish
⚠️ WARNING: Manual publishing bypasses CI detection guards and repository verification. Use canonical script unless you have a specific reason not to.
The canonical publish script (scripts/publish-to-pypi.sh) includes CI detection guards to prevent accidental execution in CI/CD pipelines.
$CI - Generic CI indicator$GITHUB_ACTIONS - GitHub Actions$GITLAB_CI - GitLab CI$JENKINS_URL - Jenkins$CIRCLECI - CircleCIIf any CI variable detected, script exits with error:
❌ ERROR: This script must ONLY be run on your LOCAL machine
Detected CI environment variables:
- CI: true
- GITHUB_ACTIONS: <not set>
...
This project enforces LOCAL-ONLY PyPI publishing for:
- Security: No long-lived PyPI tokens in GitHub secrets
- Speed: 30 seconds locally vs 3-5 minutes in CI
- Control: Manual approval step before production release
See: docs/development/PUBLISHING.md (ADR-0027)
# This should FAIL with error message
CI=true ./scripts/publish-to-pypi.sh
# Expected: ❌ ERROR: This script must ONLY be run on your LOCAL machine
Project: claude-config
Configs: prd (production), dev (development)
Secret Name: PYPI_TOKEN
Valid PyPI token format:
pypi-AgEIcHlwaS5vcmcpypi-AgEIcHlwaS5vcmcCJGI4YmNhMDA5LTg...Account-wide token (recommended):
Project-scoped token:
# 1. Create new token on PyPI
# Visit: https://pypi.org/manage/account/token/
# 2. Update Doppler
doppler secrets set PYPI_TOKEN='new-token' \
--project claude-config \
--config prd
# 3. Verify new token works
doppler secrets get PYPI_TOKEN \
--project claude-config \
--config prd \
--plain
# 4. Test publish (dry-run not available, use TestPyPI)
# See: Troubleshooting → TestPyPI Testing
Symptom: Script fails at Step 0
Fix:
# Verify token exists
doppler secrets --project claude-config --config prd | grep PYPI_TOKEN
# If missing, get new token from PyPI
# Visit: https://pypi.org/manage/account/token/
# Create token with scope: "Entire account" or specific project
# Store in Doppler
doppler secrets set PYPI_TOKEN='your-token' \
--project claude-config \
--config prd
Symptom: Script fails at Step 4 with authentication error
Root Cause: Token expired or invalid (PyPI requires 2FA since 2024)
Fix:
doppler secrets set PYPI_TOKEN='new-token' --project claude-config --config prdSymptom:
❌ ERROR: This script must ONLY be run on your LOCAL machine
Detected CI environment variables:
- CI: true
Root Cause: Running in CI environment OR CI variable set locally
Fix:
# Check if CI variable set in your shell
env | grep CI
# If set, unset it
unset CI
unset GITHUB_ACTIONS
# Retry publish
./scripts/publish-to-pypi.sh
Expected behavior: This is INTENTIONAL - script should ONLY run locally.
Symptom: Local publish uses old version number
Root Cause: Didn't pull latest release commit from GitHub
Fix:
# Always pull before publishing
git pull origin main
# Verify version updated
grep '^version = ' pyproject.toml
# Retry publish
./scripts/publish-to-pypi.sh
Symptom: Script fails at startup before any steps
Root Cause: uv not installed or not discoverable
How the script discovers uv (in priority order):
~/.local/bin/uv, ~/.cargo/bin/uv, /opt/homebrew/bin/uv)Fix: Install uv using any method:
# Official installer (recommended)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Homebrew
brew install uv
# Cargo
cargo install uv
# mise (if you use it)
mise use uv@latest
The script doesn't force any particular installation method.
Symptom: Script starts but produces no output, eventually times out
Root Cause: Script sources ~/.zshrc or ~/.bashrc which waits for interactive input
Fix: Never source shell config files in scripts. The bundled script uses:
# CORRECT - safe for non-interactive shells
eval "$(mise activate bash 2>/dev/null)" || true
# WRONG - hangs in non-interactive shells
source ~/.zshrc
To test publishing workflow without affecting production:
Get TestPyPI token:
Store in Doppler (separate key):
doppler secrets set TESTPYPI_TOKEN='your-test-token' \
--project claude-config \
--config prd
Modify publish script temporarily:
# In scripts/publish-to-pypi.sh, change:
uv publish --token "${PYPI_TOKEN}"
# To:
TESTPYPI_TOKEN=$(doppler secrets get TESTPYPI_TOKEN --plain)
uv publish --repository testpypi --token "${TESTPYPI_TOKEN}"
Test publish:
./scripts/publish-to-pypi.sh
Verify on TestPyPI:
Restore script to production configuration
docs/architecture/decisions/0027-local-only-pypi-publishing.md - Architectural decision for local-only publishingdocs/architecture/decisions/0028-skills-documentation-alignment.md - Skills alignment with ADR-0027docs/development/PUBLISHING.md - Complete release workflow guidesemantic-release - Versioning automation (NO publishing)scripts/publish-to-pypi.sh - Reference implementation with CI guardsdiscover_uv() checks PATH → direct installs → version managers (priority order)Last Updated: 2025-12-03 Policy: Workspace-wide local-only PyPI publishing (ADR-0027) Supersedes: None (created with ADR-0027 compliance from start)