forked from ruvnet/RuView
-
Notifications
You must be signed in to change notification settings - Fork 0
363 lines (347 loc) · 15.5 KB
/
Copy pathpip-release.yml
File metadata and controls
363 lines (347 loc) · 15.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
# ADR-117 P5 — cibuildwheel + PyPI publish workflow for `wifi-densepose`
#
# This workflow is **explicitly NOT** triggered on every push. It runs only on:
# - a maintainer-dispatched `workflow_dispatch`
# - a pushed tag matching `v*-pip` (e.g. `v2.0.0-pip`)
#
# The reason for the `-pip` tag suffix is that the repo already cuts
# `v0.X.Y-esp32` tags for firmware releases (see CLAUDE.md). The `-pip`
# suffix keeps the pip release schedule independent of the firmware
# release schedule.
#
# Sequencing on release day (per ADR-117 §7.3):
# 1. cut tag `v1.99.0-pip` → publishes the tombstone wheel first
# 2. cut tag `v2.0.0-pip` → publishes the PyO3 v2 wheel matrix
#
# Publishes via the `PYPI_API_TOKEN` GitHub Actions secret (API-token
# auth). This is the ACTIVE, working publish path — the token is sourced
# fresh from GCP Secret Manager per the runbook in
# docs/integrations/pypi-release.md (GCP Secret Manager → gh secret set),
# which also keeps KICS from flagging the secret name as a generic-secret
# literal here.
#
# TODO(ADR-184 P1b): migrate to PyPI OIDC Trusted Publishing to remove
# this rotatable/expire-able credential. That switch is GATED on a manual
# pypi.org step no CLI/agent can perform: the repo owner must register a
# Trusted Publisher on pypi.org for owner=ruvnet / repo=RuView /
# workflow=pip-release.yml (BOTH the wifi-densepose and ruview projects;
# ruview as a pending publisher) — see docs/adr/ADR-184-*.md. Do NOT grant
# the OIDC id-token write permission before that registration exists, or
# publishing fails with "no trusted publisher configured" — a silent
# regression the `Verify fix markers` guard `RuView#786-pypi-token-auth`
# exists to catch (it forbids that permission string in this file). When
# the owner confirms both entries are live, do the OIDC switch as a
# dedicated follow-up commit (drop `password:`, add the OIDC id-token
# permission + `environment: pypi`) so there is no capability gap between.
#
# Production publishing fails closed until the ADR-117 §11.3 v2 witness
# hash exists. TestPyPI remains usable to validate release artifacts.
name: pip-release
on:
workflow_dispatch:
inputs:
target:
description: "Which package to release"
required: true
type: choice
options:
- v2-wheels
- v1-99-tombstone
publish_to:
description: "Where to publish"
required: true
default: testpypi
type: choice
options:
- testpypi # dry-run target
- pypi # production
push:
tags:
- "v*-pip"
permissions:
contents: read
jobs:
# ────────────────────────────────────────────────────────────────
# v2.0.0 — cibuildwheel matrix (5 wheels + sdist)
# ────────────────────────────────────────────────────────────────
build-wheels:
name: Build ${{ matrix.os }} ${{ matrix.arch }}
if: |
github.event_name == 'workflow_dispatch' && inputs.target == 'v2-wheels' ||
startsWith(github.ref, 'refs/tags/v2.')
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: x86_64
- os: ubuntu-latest
arch: aarch64
- os: macos-15-intel # x86_64 runner
arch: x86_64
- os: macos-14 # arm64 runner
arch: arm64
- os: windows-latest
arch: AMD64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
# Linux aarch64 needs QEMU for cross-build on x86_64 runners.
- name: Set up QEMU
if: matrix.os == 'ubuntu-latest' && matrix.arch == 'aarch64'
uses: docker/setup-qemu-action@v3
# ADR-117 §5.4: abi3-py310 — one binary per OS/arch covers all
# Python minor versions ≥ 3.10. Build only cp310 wheels.
- name: Build wheels (cibuildwheel)
uses: pypa/cibuildwheel@v2.21
env:
CIBW_BUILD: "cp310-*"
CIBW_ARCHS_LINUX: ${{ matrix.arch }}
CIBW_ARCHS_MACOS: ${{ matrix.arch }}
CIBW_ARCHS_WINDOWS: ${{ matrix.arch }}
CIBW_BUILD_FRONTEND: "build"
CIBW_BEFORE_BUILD: "pip install maturin>=1.7"
# The PyO3 sdist landing depends on the cargo/Rust toolchain
# being present. cibuildwheel images carry rustup on Linux
# but we also pin a known-good version for reproducibility.
CIBW_BEFORE_ALL_LINUX: "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.82"
CIBW_ENVIRONMENT_LINUX: 'PATH="$HOME/.cargo/bin:$PATH"'
# Smoke-test every built wheel before accepting it. Catches
# the case where the wheel imports but the compiled symbols
# are missing.
CIBW_TEST_REQUIRES: "pytest>=8.0"
CIBW_TEST_COMMAND: 'python -c "import wifi_densepose; assert wifi_densepose.hello() == \"ok\"; print(wifi_densepose.__build_features__)"'
with:
package-dir: python
output-dir: wheelhouse
- uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}-${{ matrix.arch }}
path: wheelhouse/*.whl
if-no-files-found: error
build-sdist:
name: Build v2 sdist
if: |
github.event_name == 'workflow_dispatch' && inputs.target == 'v2-wheels' ||
startsWith(github.ref, 'refs/tags/v2.')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install maturin
run: pip install maturin>=1.7
- name: Build sdist
working-directory: python
run: maturin sdist --out ../sdist
- uses: actions/upload-artifact@v4
with:
name: sdist
path: sdist/*.tar.gz
if-no-files-found: error
build-ruview:
name: Build ruview meta-package
if: |
github.event_name == 'workflow_dispatch' && inputs.target == 'v2-wheels' ||
startsWith(github.ref, 'refs/tags/v2.')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Verify lock-step package versions
shell: python
run: |
import pathlib
import tomllib
root = pathlib.Path("python")
core = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))
meta = tomllib.loads((root / "ruview-meta" / "pyproject.toml").read_text(encoding="utf-8"))
core_version = core["project"]["version"]
meta_version = meta["project"]["version"]
expected_dependency = f"wifi-densepose=={core_version}"
if meta_version != core_version:
raise SystemExit(
f"package versions differ: wifi-densepose={core_version}, ruview={meta_version}"
)
if expected_dependency not in meta["project"]["dependencies"]:
raise SystemExit(f"ruview must depend on {expected_dependency}")
print(f"lock-step version: {core_version}")
- name: Build ruview wheel and sdist
run: |
python -m pip install --upgrade pip build
python -m build python/ruview-meta --outdir ruview-dist
- uses: actions/upload-artifact@v4
with:
name: ruview
path: ruview-dist/*
if-no-files-found: error
# ────────────────────────────────────────────────────────────────
# v1.99.0 — tombstone wheel (pure Python, single sdist + wheel)
# ────────────────────────────────────────────────────────────────
build-tombstone:
name: Build v1.99.0 tombstone
if: |
github.event_name == 'workflow_dispatch' && inputs.target == 'v1-99-tombstone' ||
startsWith(github.ref, 'refs/tags/v1.99')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install build backend
run: python -m pip install --upgrade pip build>=1.2
- name: Build sdist + wheel
working-directory: python/tombstone
run: python -m build --outdir ../../tombstone-dist
# Inspect what was actually built — the previous v1.99.0-pip run
# showed an `import wifi_densepose` that returned cleanly instead
# of raising, even though build logs said `adding 'wifi_densepose/__init__.py'`.
# Print the wheel manifest + the __init__.py content so any
# future regression is debuggable from the run log alone.
- name: Inspect wheel contents
run: |
set -e
WHL=tombstone-dist/wifi_densepose-1.99.0-py3-none-any.whl
echo "--- wheel listing ---"
python -m zipfile -l "$WHL"
echo "--- wifi_densepose/__init__.py inside the wheel ---"
python -m zipfile -e "$WHL" /tmp/tomb-inspect
cat /tmp/tomb-inspect/wifi_densepose/__init__.py
echo "--- size in bytes ---"
wc -c /tmp/tomb-inspect/wifi_densepose/__init__.py
# Smoke-test in an ISOLATED venv. The previous run's failure
# mode was that the ubuntu-latest runner's system `python` had
# site-packages picking up something other than the user-installed
# wheel, so the import resolved to a different module. A clean
# venv removes any ambiguity about which wifi_densepose is loaded.
- name: Smoke-test tombstone in isolated venv
run: |
set -e
# Copy the wheel to /tmp BEFORE entering the venv — we must
# cd OUT of the repo root because the repo contains a
# `wifi_densepose/` directory left over from the legacy v1
# source. Python puts cwd at sys.path[0], so an import from
# the repo root would resolve to the legacy directory and
# bypass the freshly-installed wheel entirely (this was the
# silent failure mode of the previous two run attempts).
cp tombstone-dist/wifi_densepose-1.99.0-py3-none-any.whl /tmp/
python -m venv /tmp/smoke-venv
/tmp/smoke-venv/bin/python -m pip install --upgrade pip
/tmp/smoke-venv/bin/python -m pip install /tmp/wifi_densepose-1.99.0-py3-none-any.whl
cd /tmp # away from the repo root's stray wifi_densepose/
/tmp/smoke-venv/bin/python -c "import importlib.util as u; s = u.find_spec('wifi_densepose'); print('Resolved to:', s.origin); print('--- file content ---'); print(open(s.origin).read())"
set +e
/tmp/smoke-venv/bin/python -c "import wifi_densepose" 2> import-output.txt
rc=$?
set -e
if [ "$rc" -eq 0 ]; then
echo "ERROR: tombstone import succeeded — should have raised ImportError"
exit 1
fi
if ! grep -q "github.com/ruvnet/RuView" import-output.txt; then
echo "ERROR: tombstone ImportError missing migration URL"
cat import-output.txt
exit 1
fi
echo "Tombstone wheel correctly raises ImportError with migration URL."
- uses: actions/upload-artifact@v4
with:
name: tombstone
path: tombstone-dist/*
if-no-files-found: error
# ────────────────────────────────────────────────────────────────
# Publish — gated by manual dispatch OR by the tag form
# ────────────────────────────────────────────────────────────────
publish-v2:
name: Publish wifi-densepose + ruview
needs: [build-wheels, build-sdist, build-ruview]
if: |
always() &&
needs.build-wheels.result == 'success' &&
needs.build-sdist.result == 'success' &&
needs.build-ruview.result == 'success' &&
(
github.event_name == 'workflow_dispatch' && inputs.target == 'v2-wheels' ||
startsWith(github.ref, 'refs/tags/v2.')
)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enforce production witness gate
if: |
startsWith(github.ref, 'refs/tags/v2.') ||
(github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi')
run: |
test -s archive/v1/data/proof/expected_features_v2.sha256 || {
echo "::error::ADR-117 §11.3 release gate is incomplete: archive/v1/data/proof/expected_features_v2.sha256 is missing or empty"
exit 1
}
- name: Gather all artifacts into dist/
uses: actions/download-artifact@v4
with:
path: dist-staging
- name: Flatten artifacts
run: |
mkdir -p dist
find dist-staging -type f \( -name '*.whl' -o -name '*.tar.gz' \) -exec cp -v {} dist/ \;
ls -lh dist/
# API-token auth (active path). See TODO(ADR-184 P1b) in the header
# before replacing `password:` with the OIDC id-token permission.
- name: Publish to TestPyPI (dry-run target)
if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi'
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.TESTPYPI_API_TOKEN }}
packages-dir: dist
skip-existing: true
- name: Publish to PyPI
if: |
startsWith(github.ref, 'refs/tags/v2.') ||
(github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi')
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist
verbose: true
publish-tombstone:
name: Publish v1.99 tombstone
needs: [build-tombstone]
if: |
always() &&
needs.build-tombstone.result == 'success' &&
(
github.event_name == 'workflow_dispatch' && inputs.target == 'v1-99-tombstone' ||
startsWith(github.ref, 'refs/tags/v1.99')
)
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: tombstone
path: dist
# API-token auth (active path). See TODO(ADR-184 P1b) in the header
# before replacing `password:` with the OIDC id-token permission.
- name: Publish to TestPyPI (dry-run target)
if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi'
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.TESTPYPI_API_TOKEN }}
packages-dir: dist
skip-existing: true
- name: Publish to PyPI
if: |
startsWith(github.ref, 'refs/tags/v1.99') ||
(github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi')
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist