-
Notifications
You must be signed in to change notification settings - Fork 6
300 lines (275 loc) · 10.6 KB
/
python-publish.yml
File metadata and controls
300 lines (275 loc) · 10.6 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
name: python-publish
# Builds + publishes the docx-scalpel PyPI distribution.
#
# Tag scheme: `docx-scalpel-v<PEP440>` (e.g. `docx-scalpel-v0.1.0a1`). This is
# deliberately decoupled from publish.yml's `v*` trigger so docx-scalpel can
# ship Python-only point releases without dragging Docxodus core / npm /
# binaries along, and so a Docxodus core release doesn't force a PyPI version
# bump for unrelated work. Compatibility is handled by bundling: each wheel
# embeds a docxodus-pyhost built from the same commit the tag points to.
#
# Matrix builds wheels for linux-x64, linux-arm64, osx-arm64, win-x64
# (each carrying a self-contained docxodus-pyhost for its RID) plus a single
# sdist. The publish job collects every dist-* artifact and uploads them as
# one PyPI release. fail-fast: false in the matrix surfaces all RID failures
# at once, but publish still gates on every matrix entry succeeding.
on:
push:
tags:
- 'docx-scalpel-v*'
workflow_dispatch:
inputs:
version:
description: 'Version to publish (PEP 440, e.g. 0.1.0a1, 1.0.0)'
required: true
type: string
target:
description: 'PyPI target — use testpypi to dry-run before a real release'
required: true
default: testpypi
type: choice
options:
- testpypi
- pypi
permissions:
contents: read
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_NOLOGO: true
jobs:
resolve:
name: Resolve version + target
runs-on: ubuntu-latest
outputs:
version: ${{ steps.out.outputs.version }}
target: ${{ steps.out.outputs.target }}
steps:
- id: out
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
ver='${{ inputs.version }}'
tgt='${{ inputs.target }}'
else
# docx-scalpel-vX.Y.Z tag → production PyPI
ver="${GITHUB_REF#refs/tags/docx-scalpel-v}"
tgt="pypi"
fi
# PEP 440 sanity: reject empty and obvious shell metacharacters.
if [[ -z "$ver" || "$ver" == *[\$\`\"\'\\\;]* ]]; then
echo "::error::invalid version: $ver"; exit 1
fi
echo "version=$ver" >> "$GITHUB_OUTPUT"
echo "target=$tgt" >> "$GITHUB_OUTPUT"
echo "Resolved: version=$ver target=$tgt"
build-wheel:
name: Build wheel (${{ matrix.rid }})
needs: resolve
runs-on: ${{ matrix.runner }}
strategy:
# One RID's failure shouldn't hide the others' — surface all problems at
# once so we can fix them in a single follow-up. publish still gates on
# ALL matrix entries succeeding (GH Actions matrix-job semantics), so a
# broken RID still blocks the release.
fail-fast: false
matrix:
include:
- rid: linux-x64
runner: ubuntu-latest
wheel-platform: manylinux_2_28_x86_64
binary: docxodus-pyhost
- rid: linux-arm64
# GitHub-hosted free ARM runner (GA for public repos since 2025).
runner: ubuntu-22.04-arm
wheel-platform: manylinux_2_28_aarch64
binary: docxodus-pyhost
# osx-x64 was retired from the matrix in 2026 once macos-13 (Intel)
# runners stopped serving the public-repo pool. Cross-publishing from
# macos-14 via Rosetta was attempted and didn't pan out; Intel Mac
# share is small enough that dropping the dedicated wheel is the
# right tradeoff. Intel Mac users can `pip install` the sdist if they
# need it, or run a docx-scalpel install with `DOCXODUS_HOST` pointed
# at a locally-built host.
- rid: osx-arm64
# Apple Silicon runner.
runner: macos-14
wheel-platform: macosx_11_0_arm64
binary: docxodus-pyhost
- rid: win-x64
runner: windows-latest
wheel-platform: win_amd64
binary: docxodus-pyhost.exe
defaults:
run:
# Use bash uniformly — windows-latest ships Git Bash, which handles the
# mkdir / cp / chmod / heredoc / glob syntax exactly the way the unix
# runners do. Saves us from forking the script per platform.
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Pin pyproject version
run: |
python - <<'PY'
import os, re, pathlib
ver = os.environ["VER"]
p = pathlib.Path("python/pyproject.toml")
src = p.read_text()
new = re.sub(r'^version\s*=\s*".*"', f'version = "{ver}"', src, count=1, flags=re.M)
p.write_text(new)
print("Set pyproject version to", ver)
PY
env:
VER: ${{ needs.resolve.outputs.version }}
- name: Publish docxodus-pyhost (${{ matrix.rid }}, self-contained, single-file)
run: |
dotnet publish tools/python-host/pyhost.csproj \
-c Release \
-r '${{ matrix.rid }}' \
--self-contained true \
-p:PublishSingleFile=true \
-p:PublishReadyToRun=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-o 'python/vendor/${{ matrix.rid }}'
- name: Stage binary into wheel layout
run: |
set -euo pipefail
mkdir -p python/src/docx_scalpel/_bin
cp "python/vendor/${{ matrix.rid }}/${{ matrix.binary }}" \
"python/src/docx_scalpel/_bin/${{ matrix.binary }}"
# chmod is a no-op on NTFS but a hard requirement on unix runners
# and harmless to run unconditionally inside Git Bash.
chmod +x "python/src/docx_scalpel/_bin/${{ matrix.binary }}" || true
ls -la python/src/docx_scalpel/_bin/
- name: Smoke-test the bundled host
run: |
set -euo pipefail
out=$(printf '{"id":1,"op":"ping","args":{}}\n{"id":2,"op":"shutdown","args":{}}\n' \
| "python/src/docx_scalpel/_bin/${{ matrix.binary }}")
echo "$out"
echo "$out" | grep -q '"pong":true' || { echo "ping failed"; exit 1; }
- name: Build wheel
run: |
set -euo pipefail
cd python
python -m pip install --upgrade pip build wheel
python -m build --wheel
ls -la dist/
- name: Retag wheel for ${{ matrix.wheel-platform }}
run: |
set -euo pipefail
cd python/dist
# Use the wheel project's own CLI (PEP 491-aware) — updates filename
# AND the embedded WHEEL metadata, not just the file rename.
python -m wheel tags --remove \
--platform-tag='${{ matrix.wheel-platform }}' \
"docx_scalpel-${VER}-py3-none-any.whl"
ls -la
env:
VER: ${{ needs.resolve.outputs.version }}
- name: Run lifecycle tests against the built wheel
run: |
set -euo pipefail
# $RUNNER_TEMP is set by GH Actions on every platform — /tmp wouldn't
# exist on windows-latest.
venv="$RUNNER_TEMP/verify-venv"
python -m venv "$venv"
# venv bin layout: unix has bin/, windows has Scripts/
if [[ -d "$venv/Scripts" ]]; then
bin="$venv/Scripts"
else
bin="$venv/bin"
fi
# `pip install --upgrade pip` via pip.exe fails on Windows because
# the running exe holds a lock on itself; pip 25+ refuses and tells
# you to use `python -m pip` instead. Use that form on every OS.
"$bin/python" -m pip install --upgrade pip
"$bin/pip" install python/dist/*.whl pytest
# Force the locator to use the wheel's bundled host, not any dev
# fallback the runner might have lying around.
unset DOCXODUS_HOST
"$bin/pytest" python/tests/test_lifecycle.py -v
- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.rid }}
path: python/dist/*.whl
if-no-files-found: error
build-sdist:
name: Build sdist
needs: resolve
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Pin pyproject version
run: |
python - <<'PY'
import os, re, pathlib
ver = os.environ["VER"]
p = pathlib.Path("python/pyproject.toml")
src = p.read_text()
new = re.sub(r'^version\s*=\s*".*"', f'version = "{ver}"', src, count=1, flags=re.M)
p.write_text(new)
PY
env:
VER: ${{ needs.resolve.outputs.version }}
- name: Build sdist
run: |
cd python
python -m pip install --upgrade pip build
python -m build --sdist
ls -la dist/
- uses: actions/upload-artifact@v4
with:
name: dist-sdist
path: python/dist/*.tar.gz
if-no-files-found: error
publish:
name: Publish to ${{ needs.resolve.outputs.target }}
needs:
- resolve
- build-wheel
- build-sdist
runs-on: ubuntu-latest
# The environment must be configured in repo Settings → Environments AND
# registered as a trusted publisher on PyPI/TestPyPI. See python/RELEASING.md.
environment:
name: ${{ needs.resolve.outputs.target }}
url: ${{ needs.resolve.outputs.target == 'testpypi' && 'https://test.pypi.org/p/docx-scalpel' || 'https://pypi.org/p/docx-scalpel' }}
permissions:
id-token: write # OIDC for trusted publishing
contents: read
steps:
- name: Download wheel + sdist
uses: actions/download-artifact@v4
with:
path: dist
pattern: dist-*
merge-multiple: true
- name: Inventory artifacts
run: |
ls -la dist/
test -n "$(ls dist/*.whl 2>/dev/null)" || { echo "no wheels found"; exit 1; }
test -n "$(ls dist/*.tar.gz 2>/dev/null)" || { echo "no sdist found"; exit 1; }
- name: Publish to TestPyPI
if: needs.resolve.outputs.target == 'testpypi'
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
packages-dir: dist/
print-hash: true
# Tolerate re-uploads — useful while iterating on the workflow.
skip-existing: true
- name: Publish to PyPI
if: needs.resolve.outputs.target == 'pypi'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
print-hash: true