mirror of
https://github.com/ruvnet/RuView
synced 2026-07-26 18:01:48 +00:00
3ed43e9a2f
Three coupled defects, all found by building the wheel and running the real
suite on Apple Silicon rather than trusting green CI.
1. THE P6 SOTA BINDINGS NEVER REACHED USERS (release blocker).
`pip-release.yml`'s cibuildwheel built the DEFAULT feature set, so published
wheels contained none of aether/mat/meridian. `pip install
wifi-densepose[aether]` then raised ImportError — and the extra is empty, so
its own error message ("install the [aether] extra") sent users in a circle.
A pip extra cannot enable a Rust cargo feature on an already-built wheel, so
the only way P6 reaches PyPI is to compile it in.
Fix: the RELEASE build opts in via `MATURIN_PEP517_ARGS="--features sota"`
(per-platform, since CIBW_ENVIRONMENT_LINUX overrides CIBW_ENVIRONMENT).
`default = []` in Cargo.toml, the RuView#1387-default-wheel-budget-config
fix-marker, and the wheel-size-budget job are all left UNTOUCHED — they keep
guarding the small base compile. Measured published wheel: 1.68 MiB, well
under the ADR-117 §5.4 5 MiB budget. Proven: built via the exact PEP517 path,
`import wifi_densepose.aether/mat/meridian` all succeed.
2. THE WHEEL SMOKE TEST COULD NOT SEE #1.
CIBW_TEST_COMMAND only asserted `hello()` and PRINTED `__build_features__`.
The one signal that would reveal missing bindings was dumped to stdout and
ignored, so a featureless wheel published green. It now ASSERTS the p6
features are present and imports the three modules. Proven red→green: fails
on the old featureless wheel ("missing SOTA bindings: [...]"), passes on the
fixed one.
3. THE AETHER PARITY TESTS WERE NOT PORTABLE ACROSS THE WHEEL MATRIX.
`test_aether.py` and the native `aether_parity.rs`/`aether_weights_parity.rs`
hashed the raw f32 embedding bytes (SHA-256) against a committed golden. The
embedding is pure f32 with transcendental ops (ln/sqrt/cos) that are not
bit-reproducible across CPUs/libm, so the hash only ever matched the one arch
that generated it. This passed in python-ci (x86) but fails on the aarch64 /
macOS-arm wheels this same project ships — latent until #1 is fixed and the
bindings actually load. Every tolerance/behavioral assertion already passed;
only the two byte-hash tests failed, which is the signature of a non-portable
golden, not a logic bug.
Fix: compare to a committed golden VECTOR within tolerance
(atol=rtol=1e-4 — ~100x cross-arch f32 drift, ~100x under any real algorithm
change), on both the Python and native sides against the SAME golden. Native
≈ golden and binding ≈ golden together prove binding ≈ native, portably.
`.sha256` goldens replaced by `.json` vectors.
Also: the aether/mat/meridian import shims told users to `pip install
wifi-densepose[<x>]` on a missing feature — an empty extra that cannot help.
Corrected to name the real fix (rebuild with `--features <x>`); message tests
updated to assert the honest message and forbid the misleading one.
Verified on aarch64/macOS: full `python/tests/` suite 227 passed against a wheel
built with the new mechanism; `cargo check --tests --features aether` clean.
The native `.rs` parity tests were converted by inspection and cargo-checked but
not executed — they link against the PyO3 crate and no workflow runs them today
(a pre-existing gap; wiring `cd python && cargo test --features sota` into
python-ci would make the native anchor actually run).
Co-Authored-By: Ruflo & AQE
330 lines
15 KiB
YAML
330 lines
15 KiB
YAML
# 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.
|
|
#
|
|
# Q3 (witness hash v2 — open in ADR-117 §11.3) MUST be resolved
|
|
# before the first v2.0.0 publish. When v2 lands, add a parallel
|
|
# step that verifies the v2 hash against the Rust pipeline.
|
|
|
|
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-13 # 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"
|
|
# PUBLISHED wheels carry the full SOTA feature set. A pip extra
|
|
# (`[aether]`) CANNOT enable a Rust cargo feature on an already-built
|
|
# wheel, so the only way P6 reaches PyPI users is to compile it in.
|
|
# `default = []` stays in Cargo.toml (dev builds + the wheel-size
|
|
# budget job keep guarding the small base compile); the RELEASE build
|
|
# opts in here via maturin's PEP517 args. Measured 1.68 MiB — well
|
|
# under the ADR-117 §5.4 5 MiB budget. Set per-platform because
|
|
# CIBW_ENVIRONMENT_LINUX overrides the general CIBW_ENVIRONMENT.
|
|
CIBW_ENVIRONMENT: 'MATURIN_PEP517_ARGS="--features sota"'
|
|
# 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" MATURIN_PEP517_ARGS="--features sota"'
|
|
# Smoke-test every built wheel before accepting it. This ASSERTS the
|
|
# SOTA bindings are present and importable — the prior version only
|
|
# printed __build_features__ and asserted hello(), so a wheel missing
|
|
# every P6 binding published green. A featureless wheel now fails here
|
|
# instead of shipping an ImportError to users.
|
|
CIBW_TEST_REQUIRES: "pytest>=8.0"
|
|
CIBW_TEST_COMMAND: >-
|
|
python -c "import wifi_densepose as w;
|
|
assert w.hello() == 'ok';
|
|
missing = [f for f in ('p6-aether-bindings','p6-meridian-bindings','p6-mat-bindings') if f not in w.__build_features__];
|
|
assert not missing, ('published wheel is missing SOTA bindings: ' + str(missing) + ' have=' + str(w.__build_features__));
|
|
import wifi_densepose.aether, wifi_densepose.mat, wifi_densepose.meridian;
|
|
print('OK — SOTA bindings present:', w.__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
|
|
|
|
# ────────────────────────────────────────────────────────────────
|
|
# 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 v2 wheels
|
|
needs: [build-wheels, build-sdist]
|
|
if: |
|
|
always() &&
|
|
needs.build-wheels.result == 'success' &&
|
|
needs.build-sdist.result == 'success' &&
|
|
(
|
|
github.event_name == 'workflow_dispatch' && inputs.target == 'v2-wheels' ||
|
|
startsWith(github.ref, 'refs/tags/v2.')
|
|
)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- 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.PYPI_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
|
|
|
|
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.PYPI_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
|