-
Notifications
You must be signed in to change notification settings - Fork 2
195 lines (170 loc) · 7.27 KB
/
Copy pathrelease.yml
File metadata and controls
195 lines (170 loc) · 7.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# =============================================================================
# coding-proxy: PyPI Publishing Pipeline (3-Stage Unified Pipeline)
# =============================================================================
# Trigger: GitHub Release publication event (release.types: [published])
#
# Architecture: 3-Stage Serial Pipeline (prerelease only — 所有生产发布均走预发布)
#
# Stage 1 (build): 矩阵构建 3.12/3.13/3.14,上传 artifacts
# Stage 2 (publish-testpypi): 发布到 TestPyPI 供验证
# Stage 3 (publish-to-pypi): ⏸ Production Approval Gate (environment: pypi)
# 审批通过后自动:Promote Release + Publish to PyPI
#
# 设计决策 — Stage 3 合并审批与发布的理由:
# pypa/gh-action-pypi-publish@release/v1 始终优先尝试 OIDC Trusted Publishing。
# 若将审批与发布拆分为两个 job,则无法同时满足:
# - 单一人工审批(不能两个 job 都声明 environment: pypi 触发二次审批)
# - OIDC 需要 environment: pypi 声明才能匹配 PyPI Trusted Publisher 配置
# 因此将 Approval Gate 与实际发布操作合并为同一 job,审批通过后顺序执行。
#
# Pre-requisites — 需要在 GitHub Settings 一次性手动配置:
# 1. repo → Settings → Environments → "pypi"
# → Required reviewers: 添加审批人员(Production Approval Gate 所需)
#
# Authentication:
# - TEST_PYPI_API_TOKEN: TestPyPI API Token(repository secret)
# - PYPI Trusted Publisher: 已配置 OIDC(无需 API Token,通过 environment: pypi 自动认证)
#
# 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.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment
# =============================================================================
name: Release / Publish Pipeline
on:
release:
types: [published]
permissions:
contents: read # 全局最低权限,各 job 按需 override
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false # 发布流水线不可被中断
jobs:
# ===========================================================================
# Stage 1: BUILD -- 矩阵构建,产出 sdist + wheel artifacts
# ===========================================================================
build:
name: Build distributions (py${{ matrix.python-version }})
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 ${{ matrix.python-version }}
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
# ===========================================================================
# Stage 2: PUBLISH TO TESTPYPI -- prerelease 路径专用
# ===========================================================================
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:
contents: read
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
pattern: dist-py*
path: dist/
merge-multiple: true
- name: List artifacts
run: ls -la dist/
- 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
# ===========================================================================
# Stage 3: PUBLISH TO PYPI -- Production Approval Gate + Promote + Publish
# ===========================================================================
# 设计决策:将 Approval Gate 与实际发布操作合并为同一 job。
#
# 原因:pypa/gh-action-pypi-publish@release/v1 始终优先尝试 OIDC Trusted Publishing,
# 需要 id-token: write + environment: pypi 同时满足才能通过 PyPI 认证。
# 若拆分为两个 job,则无法避免二次审批或 OIDC 认证失败。
#
# 工作流程:
# 1. GitHub Actions pre-job 机制检测到 environment: pypi → 挂起等待审批
# 2. 审批人点击 Approve → job 开始执行
# 3. 顺序执行:Promote Release → Download Artifacts → Publish to PyPI
# ===========================================================================
publish-to-pypi:
name: "⏸ Production Approval Gate → Publish to PyPI"
runs-on: ubuntu-latest
needs: publish-testpypi
if: github.event.release.prerelease == true
timeout-minutes: 1440 # 24 小时(含审批等待 + 发布执行)
environment:
name: pypi
url: https://pypi.org/p/coding-proxy
permissions:
contents: write # 更新 GitHub Release 元数据(prerelease=false, make_latest=true)
id-token: write # PyPI OIDC Trusted Publishing 所需
steps:
- name: Approval granted — proceeding to PyPI production
run: |
echo "✅ Production deployment approved"
echo "Tag: ${{ github.ref_name }}"
echo "Release: ${{ github.event.release.name }}"
- name: Promote GitHub Release to stable
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const releaseId = ${{ github.event.release.id }};
const tagName = '${{ github.ref_name }}';
await github.rest.repos.updateRelease({
owner,
repo,
release_id: releaseId,
prerelease: false,
make_latest: true,
});
core.notice(`✅ Release #${releaseId} (${tagName}) promoted: prerelease=false, make_latest=true`);
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
pattern: dist-py*
path: dist/
merge-multiple: true
- name: List artifacts
run: ls -la dist/
- name: Publish to PyPI (OIDC Trusted Publishing)
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
verbose: true