pypi-doppler
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.
$ Installer
git clone https://github.com/terrylica/cc-skills /tmp/cc-skills && cp -r /tmp/cc-skills/plugins/itp/skills/pypi-doppler ~/.claude/skills/cc-skills// tip: Run this command in your terminal to install the skill
name: pypi-doppler description: 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.
PyPI Publishing with Doppler (Local-Only)
â ïž WORKSPACE-WIDE POLICY: LOCAL-ONLY PUBLISHING
This skill supports LOCAL machine publishing ONLY.
FORBIDDEN
â 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
REQUIRED
â
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)
Rationale
- 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
- Flexibility: Centralized credential management via Doppler
See: ADR-0027, docs/development/PUBLISHING.md
Overview
This skill provides local-only PyPI publishing using Doppler for secure credential management. It integrates with the workspace-wide release workflow where:
- GitHub Actions: Automated versioning ONLY (tags, releases, CHANGELOG)
- Local Machine: Manual PyPI publishing with Doppler credentials
Bundled Scripts
| Script | Purpose |
|---|---|
scripts/publish-to-pypi.sh | Local PyPI publishing with CI detection guards |
Usage: Copy to your project's scripts/ directory:
/usr/bin/env bash << 'DOPPLER_EOF'
# 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
DOPPLER_EOF
Prerequisites
One-Time Setup
-
Install Doppler CLI:
brew install dopplerhq/cli/doppler -
Authenticate with Doppler:
doppler login -
Verify access to
claude-configproject:doppler whoami doppler projects
PyPI Token Setup
-
Create PyPI API token:
- Visit: https://pypi.org/manage/account/token/
- Enable 2FA if not already enabled (required since 2024)
- Create token with scope: "Entire account" or specific project
- Copy token (starts with
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
Publishing Workflow
MANDATORY: Verify Version Increment Before Publishing
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:
- Compare local
pyproject.tomlversion against latest PyPI version - If versions match â STOP - do not proceed with publishing
- Inform user: "Version not incremented. Run semantic-release first or verify commits include
feat:orfix: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.
Complete Release Workflow
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:
- â
Analyzes commits using
@semantic-release/commit-analyzer - â
Determines next version (e.g.,
v7.1.0) - â
Updates
pyproject.toml,package.jsonversions - â
Generates and updates
CHANGELOG.md - â
Creates git tag (
v7.1.0) - â Creates GitHub release with release notes
- â
Commits changes back to repo with
[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
Publishing Command (Local Machine Only)
CRITICAL: This command must ONLY run on your local machine, NEVER in CI/CD.
Using Bundled Script (Recommended)
/usr/bin/env bash << 'GIT_EOF'
# 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
GIT_EOF
Bundled script features:
- â CI detection guards (blocks if CI=true)
- â Repository verification (prevents fork abuse)
- â Doppler integration (PYPI_TOKEN retrieval)
- â Build + publish + verify workflow
- â Clear error messages
Manual Publishing (Advanced)
For manual publishing without the canonical script:
/usr/bin/env bash << 'CONFIG_EOF'
# 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
CONFIG_EOF
â ïž WARNING: Manual publishing bypasses CI detection guards and repository verification. Use canonical script unless you have a specific reason not to.
CI Detection Enforcement
The canonical publish script (scripts/publish-to-pypi.sh) includes CI detection guards to prevent accidental execution in CI/CD pipelines.
Environment Variables Checked
$CI- Generic CI indicator$GITHUB_ACTIONS- GitHub Actions$GITLAB_CI- GitLab CI$JENKINS_URL- Jenkins$CIRCLECI- CircleCI
Behavior
If 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)
Testing CI Detection
# 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
Credential Management
Doppler Configuration
Project: claude-config
Configs: prd (production), dev (development)
Secret Name: PYPI_TOKEN
Token Format
Valid PyPI token format:
- Starts with:
pypi-AgEIcHlwaS5vcmc - Length: ~180 characters
- Example:
pypi-AgEIcHlwaS5vcmcCJGI4YmNhMDA5LTg...
Token Permissions
Account-wide token (recommended):
- Can publish to all projects under your account
- Simpler management
- One token for all repositories
Project-scoped token:
- Can only publish to specific project
- More restrictive
- Separate token per project needed
Token Rotation
# 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
Troubleshooting
Issue: "PYPI_TOKEN not found in Doppler"
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
Issue: "403 Forbidden from PyPI"
Symptom: Script fails at Step 4 with authentication error
Root Cause: Token expired or invalid (PyPI requires 2FA since 2024)
Fix:
- Verify 2FA enabled on PyPI account
- Create new token: https://pypi.org/manage/account/token/
- Update Doppler:
doppler secrets set PYPI_TOKEN='new-token' --project claude-config --config prd - Retry publish
Issue: "Script blocked with CI detection error"
Symptom:
â 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.
Issue: "Version not updated in pyproject.toml"
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
Issue: "uv package manager not found"
Symptom: Script fails at startup before any steps
Root Cause: uv not installed or not discoverable
How the script discovers uv (in priority order):
- Already in PATH (Homebrew, direct install, shell configured)
- Common direct install locations (
~/.local/bin/uv,~/.cargo/bin/uv,/opt/homebrew/bin/uv) - Version managers as fallback (mise, asdf)
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.
Issue: Script Hangs with No Output
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:
/usr/bin/env bash << 'MISE_EOF'
# CORRECT - safe for non-interactive shells
eval "$(mise activate bash 2>/dev/null)" || true
# WRONG - hangs in non-interactive shells
source ~/.zshrc
MISE_EOF
TestPyPI Testing
To test publishing workflow without affecting production:
-
Get TestPyPI token:
- Visit: https://test.pypi.org/manage/account/token/
- Create token
-
Store in Doppler (separate key):
doppler secrets set TESTPYPI_TOKEN='your-test-token' \ --project claude-config \ --config prd -
Modify publish script temporarily:
/usr/bin/env bash << 'DOPPLER_EOF_2'
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}"
DOPPLER_EOF_2
4. **Test publish**:
```bash
./scripts/publish-to-pypi.sh
-
Verify on TestPyPI:
-
Restore script to production configuration
Related Documentation
- ADR-0027:
docs/architecture/decisions/0027-local-only-pypi-publishing.md- Architectural decision for local-only publishing - ADR-0028:
docs/architecture/decisions/0028-skills-documentation-alignment.md- Skills alignment with ADR-0027 - PUBLISHING.md:
docs/development/PUBLISHING.md- Complete release workflow guide - semantic-release Skill:
semantic-release- Versioning automation (NO publishing) - Bundled Script:
scripts/publish-to-pypi.sh- Reference implementation with CI guards
Validation History
- 2025-12-03: Refactored to discovery-first, environment-agnostic approach
discover_uv()checks PATH â direct installs â version managers (priority order)- Supports: curl install, Homebrew, cargo, mise, asdf - doesn't force any method
- Early discovery at startup before any workflow steps
- Troubleshooting for non-interactive shell issues
- 2025-11-22: Created with ADR-0027 alignment (workspace-wide local-only policy)
- Validation: CI detection guards tested, Doppler integration verified
Last Updated: 2025-12-03 Policy: Workspace-wide local-only PyPI publishing (ADR-0027) Supersedes: None (created with ADR-0027 compliance from start)
Repository
