Skip to content

Release / Publish Pipeline #34

Release / Publish Pipeline

Release / Publish Pipeline #34

Workflow file for this run

# =============================================================================
# coding-proxy: PyPI Publishing Workflow (Initial Release)
# =============================================================================
# Trigger: GitHub Release publication event
# Architecture: Build-Publish Separation (PyPA security best practice)
# Paired with: promote.yml (prerelease → stable promotion gate)
# - Job 1 (build): Low privileges, produces sdist+wheel artifacts
# - Job 2 (publish-testpypi): Publish to TestPyPI (prereleases only)
# - Job 3 (publish-pypi): Publish to PyPI (stable releases / hotfixes)
#
# Routing Logic:
# - prerelease == true --> TestPyPI (with skip-existing tolerance)
# --> THEN use promote.yml to promote to PyPI after validation
# - prerelease == false --> PyPI production (direct stable release, e.g. hotfixes)
#
# Promotion Flow (see promote.yml):
# 1. Create release with prerelease: true --> builds + publishes to TestPyPI
# 2. Human validates package on TestPyPI
# 3. Run promote.yml (workflow_dispatch) --> promotes to PyPI production
#
# Pre-requisites (choose ONE authentication method):
#
# Option A — OIDC Trusted Publishing (recommended, no secrets needed):
# 1. Create GitHub Environments: "pypi" and "testpypi"
# 2. Configure Trusted Publishers on PyPI/TestPyPI admin panels
# 3. (Recommended) Set "Required reviewers" on "pypi" environment
#
# Option B — API Token Fallback (simpler initial setup):
# 1. Set repository secrets: PYPI_API_TOKEN and/or TEST_PYPI_API_TOKEN
# 2. (Optional) Create "pypi"/"testpypi" environments for deployment guards
# If environments are not created, remove the `environment:` blocks below.
#
# References:
# [1] https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
# [2] https://github.com/pypa/gh-action-pypi-publish
# [3] https://docs.pypi.org/trusted-publishers/using-a-publisher/
# =============================================================================
name: Release / Publish to PyPI
on:
release:
types: [published]
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ===========================================================================
# Job 1: BUILD -- Produce distribution artifacts (low privilege isolation)
# ===========================================================================
build:
name: Build distributions
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set up uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- name: Install build dependencies
run: uv pip install --system build twine
- name: Build sdist and wheel
run: python -m build
- name: Check package metadata
run: twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist-py${{ matrix.python-version }}
path: dist/
retention-days: 14
# ===========================================================================
# Job 2: PUBLISH TO TESTPYPI -- Prerelease / staging releases only
# ===========================================================================
publish-testpypi:
name: Publish to TestPyPI
runs-on: ubuntu-latest
needs: build
if: github.event.release.prerelease == true
timeout-minutes: 10
environment:
name: testpypi
url: https://test.pypi.org/p/coding-proxy
permissions:
id-token: write
contents: read
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
pattern: dist-py*
path: dist/
merge-multiple: true
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository-url: https://test.pypi.org/legacy/
skip-existing: true
attestations: false # TestPyPI 不支持 attestations,必须显式禁用
verbose: true # 启用详细日志以获取 400 错误响应体
# ===========================================================================
# Job 3: PUBLISH TO PYPI -- Production releases only
# ===========================================================================
publish-pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: build
if: github.event.release.prerelease == false
timeout-minutes: 10
environment:
name: pypi
url: https://pypi.org/p/coding-proxy
permissions:
id-token: write
contents: read
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
pattern: dist-py*
path: dist/
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}