forked from waditu/czsc
-
Notifications
You must be signed in to change notification settings - Fork 0
423 lines (386 loc) · 17.5 KB
/
Copy pathpython-publish.yml
File metadata and controls
423 lines (386 loc) · 17.5 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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# Build & Publish Python Package
#
# czsc 1.0.0+ is a mixed Rust/Python package: the czsc._native extension
# is produced by the czsc-python crate (PyO3) and the pure-Python tree
# under czsc/ is bundled alongside via maturin's `python-source = "."`.
# We build per-platform abi3 wheels (Python 3.10+ ABI) so a single wheel
# per (OS, arch) covers Python 3.10/3.11/3.12/3.13.
#
# 单一版本源:Cargo.toml [workspace.package].version。
# - pyproject.toml 设 dynamic = ["version"],由 maturin 读取 Cargo.toml
# - czsc/__init__.py 通过 importlib.metadata.version("czsc") 反查
# - 本 workflow 校验 wheel 文件名版本 == git tag 版本 == Cargo.toml 版本
#
# Trusted Publishing (OIDC) 在 https://pypi.org/p/czsc 与
# https://test.pypi.org/p/czsc 两个 project 上配置,仓库不存任何 token。
name: Build & Publish Python Package
on:
push:
tags:
- 'v*' # 监听 v* 格式的 tag,例如 v1.0.0
workflow_dispatch:
inputs:
publish_to_testpypi:
description: 'Publish to TestPyPI instead of PyPI'
required: false
default: false
type: boolean
# 同一 ref(tag)只允许一份发布流程在跑,避免并发 push 多个 tag 时
# 互相竞争 PyPI 上传。cancel-in-progress=false:不打断已经在跑的,
# 但新触发的同 ref 任务会排队等。
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false
jobs:
# ------------------------------------------------------------------
# 1) Build per-platform abi3 wheels
#
# 平台覆盖:
# Linux : x86_64 (manylinux2014) + aarch64 (manylinux2014) + x86_64 (musllinux_1_2)
# macOS : x86_64 (macos-13) + aarch64 (macos-14, Apple Silicon)
# Windows: x64
# abi3-py310 → 单 wheel 覆盖 3.10/3.11/3.12/3.13
# ------------------------------------------------------------------
build-wheels:
name: Wheel ${{ matrix.platform.os }} ${{ matrix.platform.target }} ${{ matrix.platform.manylinux }}
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
platform:
- { os: ubuntu-latest, target: x86_64, manylinux: '2014' }
- { os: ubuntu-latest, target: aarch64, manylinux: '2014' }
- { os: ubuntu-latest, target: x86_64, manylinux: 'musllinux_1_2' }
# macOS x86_64 用 macos-latest runner cross-compile(macos-13 Intel
# runner pool 极度紧张,单 job 排队常 1h+ 卡 publish;macos-latest
# 当前指向 Apple Silicon runner,池子充裕)。Apple 工具链原生支持
# x86_64 cross-build,等价于 `cargo build --target x86_64-apple-darwin`。
- { os: macos-latest, target: x86_64, manylinux: '' }
- { os: macos-latest, target: aarch64, manylinux: '' }
- { os: windows-latest, target: x64, manylinux: '' }
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: '3.10'
# Rust toolchain & target cache(manylinux 容器内不复用,仅 native 路径生效)
- name: Cache cargo
if: matrix.platform.manylinux == ''
uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
key: ${{ matrix.platform.os }}-${{ matrix.platform.target }}
- name: Build wheel (maturin)
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: ${{ matrix.platform.manylinux }}
args: --release --strip --out dist
sccache: 'true'
# ring 0.17.x cross-compile 到 aarch64 manylinux2014 时,
# manylinux2014 镜像里的 cross-gcc 没把 __ARM_ARCH 宏传给 ASM,
# 导致 sha256-armv8-linux64.S 编译失败("ARM assembler must
# define __ARM_ARCH")。这里对 aarch64 manylinux build 显式
# 注入 CFLAGS_aarch64_unknown_linux_gnu,让 cc-rs 把宏带上。
# 对其他 target 这条 env 无副作用(cc-rs 按 target triple 取
# 对应 CFLAGS_<triple>),所以无需 case 分支。
before-script-linux: |
export CFLAGS_aarch64_unknown_linux_gnu="-D__ARM_ARCH=8"
- name: List built wheels
shell: bash
run: ls -la dist/
# ⚠️ ARTIFACT NAMING CONTRACT ⚠️
# 命名公式:wheels-<os>-<target>-<manylinux|native>
# 这是 build-wheels 与 smoke-test 之间的单一约定,smoke-test 用同样的
# os/target/manylinux 三字段 + 同一表达式 ${manylinux || 'native'}
# 反推 artifact 名。任何一边修改公式都必须同步另一边。
# 不要把这个表达式抽到 env,actions/upload-artifact 的 name 字段
# 不能引用 env(会被字面量化),只能在 ${{ }} 里直接拼。
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.platform.os }}-${{ matrix.platform.target }}-${{ matrix.platform.manylinux || 'native' }}
path: dist/*.whl
retention-days: 30
if-no-files-found: error
# ------------------------------------------------------------------
# 2) Build source distribution (sdist)
# ------------------------------------------------------------------
build-sdist:
name: Build sdist
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Build sdist (maturin)
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- name: List sdist
run: ls -la dist/
- name: Upload sdist artifact
uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz
retention-days: 30
if-no-files-found: error
# ------------------------------------------------------------------
# 3) Smoke-test wheels
#
# 跨 4 平台 (Linux x86_64 / macOS x86_64 / macOS arm64 / Windows x64)
# 用 --no-deps 安装 wheel,仅装最小必需依赖(pandas/numpy/loguru/
# deprecated/parse),然后实际调用 czsc._native 中的 Rust 实现,
# 确认扩展模块在该平台能加载并运行,避免被 wbt/TA-Lib 等运行时
# 重型依赖在 PyPI 上的 wheel 可用性卡死发布流程。
#
# smoke matrix 字段(os/target/manylinux)必须与 build-wheels matrix
# 保持同名,这样能复用上面 ARTIFACT NAMING CONTRACT 中的同一公式
# `wheels-<os>-<target>-<manylinux|native>` 反推 artifact,无需手动
# 维护 artifact 字符串映射表(上一版本的常见漂移源)。
#
# Linux ARM64 与 musllinux 不在 smoke 范围(GitHub-hosted runner
# 没有原生 ARM Linux / Alpine 环境,发布前可在本地 docker 验证)。
# ------------------------------------------------------------------
smoke-test:
name: Smoke ${{ matrix.smoke.os }} ${{ matrix.smoke.target }}
needs: build-wheels
runs-on: ${{ matrix.smoke.os }}
strategy:
fail-fast: false
matrix:
smoke:
- { os: ubuntu-latest, target: x86_64, manylinux: '2014' }
# macOS x86_64 wheel 是 cross-compile 出来的(见 build-wheels 注释),
# 不能在 Apple Silicon runner 上原生 import 验证(platform mismatch),
# 而 macos-13 Intel runner pool 紧张到不可用——故 CI 不 smoke 它,
# 信任 Apple 工具链 cross-build 的稳定性。
- { os: macos-latest, target: aarch64, manylinux: '' }
- { os: windows-latest, target: x64, manylinux: '' }
steps:
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Download wheel
uses: actions/download-artifact@v4
with:
# 与 build-wheels::Upload wheel artifact 共用同一命名公式
# (见上方 ARTIFACT NAMING CONTRACT 注释)
name: wheels-${{ matrix.smoke.os }}-${{ matrix.smoke.target }}-${{ matrix.smoke.manylinux || 'native' }}
path: dist/
# 最小依赖集:czsc/__init__.py + czsc/fsa/__init__.py + czsc/traders/__init__.py
# 顶层 import 链上必须的几个。
# 注意:wbt 是 czsc 顶层 import(pyproject.toml 硬依赖);requests 是 czsc.fsa
# 顶层 import;漏装任一项 smoke import czsc 会直接崩。
# 其余运行时依赖(pyarrow/polars/scipy/...)不在 import 路径上,--no-deps 跳过即可。
- name: Install + import czsc (no deps, then minimal deps)
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install --no-deps dist/*.whl
python -m pip install pandas numpy loguru deprecated parse pytz tqdm dill tenacity wbt requests
python -c "import czsc; print('czsc version:', czsc.__version__)"
python -c "from czsc import CZSC, RawBar, Freq; print('core imports OK')"
python -c "from czsc._native import signals; print('native signals namespace OK:', sorted(dir(signals))[:5])"
python -c "from czsc._native.signals import bar; print('bar signals callables:', [n for n in dir(bar) if not n.startswith('_')][:5])"
python -c "import czsc._native.ta as ta; print('rust ta module OK:', sorted(dir(ta))[:5])"
# ------------------------------------------------------------------
# 4) (optional) Publish to TestPyPI via workflow_dispatch
# ------------------------------------------------------------------
publish-to-testpypi:
name: Publish to TestPyPI
if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish_to_testpypi == 'true'
needs: [build-wheels, build-sdist, smoke-test]
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/p/czsc
permissions:
id-token: write # Trusted Publishing
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist-staging/
- name: Flatten dist
run: |
mkdir -p dist
find dist-staging -type f \( -name "*.whl" -o -name "*.tar.gz" \) -exec mv {} dist/ \;
ls -la dist/
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
# 测试场景下经常重传同版本,跳过已存在文件而非失败
skip-existing: true
verbose: true
# ------------------------------------------------------------------
# 5) Publish to PyPI on tag push
#
# 只用 push tag 触发:去掉 release.published,避免本 workflow
# 自己的 `gh release create` 又把 release.published 触发回来,
# 导致同一个版本被发布两次(第二次会因为版本已存在而失败)。
# ------------------------------------------------------------------
publish-to-pypi:
name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: [build-wheels, build-sdist, smoke-test]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/czsc
permissions:
id-token: write # Trusted Publishing
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version from tag
id: extract_version
shell: bash
run: |
TAG_NAME="${GITHUB_REF#refs/tags/}"
VERSION="${TAG_NAME#v}"
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Tag: $TAG_NAME, Version: $VERSION"
- name: Read version from Cargo.toml (single source of truth)
id: cargo_version
shell: bash
run: |
# 抽取 [workspace.package] 段下面第一个 version = "..."
# 用 awk -F'"' 切分字段(POSIX 兼容;gawk gsub 不支持 \\1 backreference,所以避免用它)
CARGO_VERSION=$(awk -F'"' '
/^\[workspace\.package\]/ {in_section=1; next}
in_section && /^\[/ {in_section=0}
in_section && /^version[[:space:]]*=/ {print $2; exit}
' Cargo.toml)
if [ -z "$CARGO_VERSION" ]; then
echo "::error::Failed to read version from Cargo.toml"
exit 1
fi
echo "version=$CARGO_VERSION" >> "$GITHUB_OUTPUT"
echo "Cargo.toml version: $CARGO_VERSION"
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist-staging/
- name: Flatten dist
run: |
mkdir -p dist
find dist-staging -type f \( -name "*.whl" -o -name "*.tar.gz" \) -exec mv {} dist/ \;
ls -la dist/
- name: Verify version consistency (tag == Cargo.toml == wheel filename)
shell: bash
run: |
TAG_VERSION="${{ steps.extract_version.outputs.version }}"
CARGO_VERSION="${{ steps.cargo_version.outputs.version }}"
# wheel 文件名格式 czsc-<VERSION>-cp<py>-abi3-<plat>.whl
# 显式锚定下一个 -cp,避免贪婪匹配把 -cp310-abi3-linux 吃进去
BUILT_VERSION=$(basename "$(ls dist/*.whl | head -1)" | sed -E 's/^czsc-([^-]+)-cp.*/\1/')
# SemVer prerelease (Cargo: 1.0.0-rc.4) 与 PEP 440 normalized form
# (PyPI wheel: 1.0.0rc4) 字符串形式不同但语义等价。maturin/setuptools
# 在打 wheel 时强制按 PEP 440 归一化,所以 Cargo SemVer 形式需要先
# 翻译到 PEP 440 再跟 wheel filename 比对。
# 翻译规则(与 packaging.version.canonicalize_version 对齐):
# -alpha.N → aN -beta.N → bN -rc.N → rcN -dev.N → .devN -post.N → .postN
NORMALIZED_CARGO=$(echo "$CARGO_VERSION" | sed -E '
s/-alpha\.([0-9]+)/a\1/;
s/-beta\.([0-9]+)/b\1/;
s/-rc\.([0-9]+)/rc\1/;
s/-dev\.([0-9]+)/.dev\1/;
s/-post\.([0-9]+)/.post\1/;
')
echo "Tag version: $TAG_VERSION"
echo "Cargo.toml version (SemVer): $CARGO_VERSION"
echo "Cargo PEP 440 normalized: $NORMALIZED_CARGO"
echo "Built wheel version: $BUILT_VERSION"
if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then
echo "::error::Tag $TAG_VERSION 与 Cargo.toml 版本 $CARGO_VERSION 不一致 —— 请同步 Cargo.toml [workspace.package].version 后重打 tag"
exit 1
fi
if [ "$BUILT_VERSION" != "$NORMALIZED_CARGO" ]; then
echo "::error::Wheel 文件名版本 $BUILT_VERSION 与 Cargo.toml PEP 440 归一化版本 $NORMALIZED_CARGO 不一致 —— maturin 元数据可能没正确读取 dynamic version"
exit 1
fi
echo "✅ Version consistency verified: tag=$TAG_VERSION cargo=$CARGO_VERSION wheel=$BUILT_VERSION"
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
# ------------------------------------------------------------------
# 6) Sign + create GitHub Release on tag push
# ------------------------------------------------------------------
create-github-release:
name: Sign and upload to GitHub Release
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: publish-to-pypi
runs-on: ubuntu-latest
permissions:
contents: write # 创建 GitHub Release
id-token: write # sigstore
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist-staging/
- name: Flatten dist
run: |
mkdir -p dist
find dist-staging -type f \( -name "*.whl" -o -name "*.tar.gz" \) -exec mv {} dist/ \;
ls -la dist/
- name: Extract version info
id: version_info
shell: bash
run: |
TAG_NAME="${GITHUB_REF#refs/tags/}"
VERSION="${TAG_NAME#v}"
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Create Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
TAG_NAME=${{ steps.version_info.outputs.tag_name }}
VERSION=${{ steps.version_info.outputs.version }}
if ! gh release view "$TAG_NAME" > /dev/null 2>&1; then
cat > release_notes.md << EOF
🚀 czsc $VERSION
### 更新内容
详细变更请查看提交历史以及 \`docs/MIGRATION_NOTES.md\`。
### 安装方式
\`\`\`bash
pip install czsc==$VERSION
\`\`\`
### 文档
- 项目文档: README.md
EOF
gh release create "$TAG_NAME" \
--title "Release $VERSION" \
--notes-file release_notes.md \
--draft=false \
--prerelease=false
else
echo "Release $TAG_NAME already exists"
fi
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.2.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Upload to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
TAG_NAME=${{ steps.version_info.outputs.tag_name }}
gh release upload "$TAG_NAME" dist/** --clobber