diff --git a/.github/workflows/pip-release.yml b/.github/workflows/pip-release.yml index 6e44e442..c985b07a 100644 --- a/.github/workflows/pip-release.yml +++ b/.github/workflows/pip-release.yml @@ -13,13 +13,10 @@ # 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 PyPI API token stored in the `PYPI_API_TOKEN` -# GitHub Actions secret. The token value comes from the GCP Secret -# Manager entry `PYPI_TOKEN` in project `cognitum-20260110`; refresh -# with: -# gcloud secrets versions access latest --secret=PYPI_TOKEN \ -# --project=cognitum-20260110 \ -# | gh secret set PYPI_API_TOKEN --repo ruvnet/RuView +# Publishes via the `PYPI_API_TOKEN` GitHub Actions secret. The +# token-refresh runbook (GCP Secret Manager → gh secret set) lives in +# docs/integrations/pypi-release.md so KICS does not flag the +# secret name as a generic-secret literal in the workflow. # # 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 diff --git a/docs/integrations/pypi-release.md b/docs/integrations/pypi-release.md new file mode 100644 index 00000000..3c4e4cad --- /dev/null +++ b/docs/integrations/pypi-release.md @@ -0,0 +1,64 @@ +# PyPI release runbook — `wifi-densepose` + `ruview` + +Operations doc for the `.github/workflows/pip-release.yml` CI workflow. + +## Auth + +The workflow uses one GitHub Actions secret named `PYPI_API_TOKEN`. +It's a project-token issued by the rUv PyPI account with upload +scope for both `wifi-densepose` and `ruview`. + +## Refreshing the token + +The canonical copy of the token lives in GCP Secret Manager, +project `cognitum-20260110`, entry name `PYPI_TOKEN`. To push a +fresh copy into GitHub Actions: + +```bash +gcloud secrets versions access latest \ + --secret=PYPI_TOKEN \ + --project=cognitum-20260110 \ + | tr -d '\r\n\xef\xbb\xbf' \ + | gh secret set PYPI_API_TOKEN --repo ruvnet/RuView +``` + +The `tr` step strips any BOM / CRLF that PowerShell pipes or +Windows editors may have introduced — without it, twine fails with +`UnicodeEncodeError: 'latin-1' codec can't encode character ''`. + +## Triggering a release + +Two paths: + +- **Tag push** — `git tag v2.X.Y-pip && git push origin v2.X.Y-pip` — + publishes the v2 wheel matrix. `v1.99.0-pip` triggers the tombstone + job instead. +- **Manual dispatch** — `gh workflow run pip-release.yml --ref + -f target=v2-wheels -f publish_to=pypi`. Use `publish_to=testpypi` + for a dry-run target if a TestPyPI token is also set as + `TESTPYPI_API_TOKEN`. + +## Release-day sequence + +Per ADR-117 §7.3, the tombstone publishes first so it claims the +"current" slot in pip's resolver: + +1. `git tag v1.99.0-pip && git push origin v1.99.0-pip` → + tombstone live at `https://pypi.org/project/wifi-densepose/1.99.0/` +2. Verify: `pip install wifi-densepose==1.99.0; python -c "import + wifi_densepose"` → ImportError with migration URL. +3. `git tag v2.0.0-pip && git push origin v2.0.0-pip` → v2 wheel + matrix live at `https://pypi.org/project/wifi-densepose/2.0.0/`. +4. (Optional, in lock-step) build + publish a matching `ruview` + release from `python/ruview-meta/` so the meta-package version + stays pinned to the same wifi-densepose version. + +## Off-loop manual gates + +- **Q3** (ADR-117 §11.3) — generate `expected_features_v2.sha256` + from the v2 Rust pipeline before any v2 publish. +- **OIDC Trusted Publisher** — not used. The workflow is token-based; + this is a deliberate choice to keep the secret refresh entirely in + GCP. If the project migrates to OIDC later, remove `password:` + from `pypa/gh-action-pypi-publish` calls and add the publisher + registration on pypi.org. diff --git a/scripts/fix-markers.json b/scripts/fix-markers.json index ba6466f1..e8375792 100644 --- a/scripts/fix-markers.json +++ b/scripts/fix-markers.json @@ -233,6 +233,46 @@ ], "rationale": "At edge tier>=2 on N16R8 PSRAM boards, process_frame() runs update_multi_person_vitals() (4 persons × 256 history samples) plus wasm_runtime_on_frame() back-to-back. The vTaskDelay(1) in edge_task() only fires AFTER process_frame() fully returns — if process_frame() takes >5 s (common on PSRAM-backed boards under sustained 30 pps CSI load), IDLE1 on Core 1 never runs and the Task Watchdog Timer fires. The fix adds two vTaskDelay(1) calls inside process_frame(), gated on tier>=2, at the multi-person vitals boundary and after WASM dispatch. Removing them re-opens the WDT storm on N16R8 hardware.", "ref": "https://github.com/ruvnet/RuView/issues/683" + }, + { + "id": "RuView#786-tombstone-import", + "title": "Tombstone (v1.99.0) __init__.py must raise ImportError with migration URL on import", + "files": ["python/tombstone/src/wifi_densepose/__init__.py"], + "require": [ + "raise ImportError(", + "pip install wifi-densepose==2.0.0", + "github.com/ruvnet/RuView" + ], + "forbid": [ + "/^def\\s/", + "/^class\\s/", + "/^import\\s+wifi_densepose/" + ], + "rationale": "ADR-117 §7.2 — the v1.99.0 tombstone wheel exists solely to raise a legible ImportError when v1.x users upgrade. If a future refactor adds real code (def / class / imports beyond the bare raise), the module may load partway before failing, breaking the migration narrative. The require patterns lock in the raise + the v2 install hint + the repo URL.", + "ref": "https://github.com/ruvnet/RuView/pull/786" + }, + { + "id": "RuView#786-tombstone-smoke-cwd", + "title": "pip-release.yml tombstone smoke-test must cd out of repo root before importing", + "files": [".github/workflows/pip-release.yml"], + "require": [ + "cd /tmp # away from the repo root's stray wifi_densepose/" + ], + "rationale": "ADR-117 §P5 — the repo root contains a legacy `./wifi_densepose/__init__.py` from v1. Python places cwd at sys.path[0], so running `import wifi_densepose` from the repo root after a fresh venv install resolves to the legacy directory and bypasses the tombstone wheel entirely. The smoke-test step MUST `cd /tmp` before the import, otherwise CI silently passes against the wrong package. This was the root cause of run 26366648768.", + "ref": "https://github.com/ruvnet/RuView/pull/786" + }, + { + "id": "RuView#786-pypi-token-auth", + "title": "pip-release.yml must authenticate to PyPI via PYPI_API_TOKEN secret, not OIDC", + "files": [".github/workflows/pip-release.yml"], + "require": [ + "password: ${{ secrets.PYPI_API_TOKEN }}" + ], + "forbid": [ + "id-token: write" + ], + "rationale": "ADR-117 §P5 — the project is registered with PyPI via API token, not OIDC Trusted Publisher. The token is sourced from GCP Secret Manager (see docs/integrations/pypi-release.md). Re-introducing the `id-token: write` permission would suggest a partial OIDC migration that won't actually work without registering the Trusted Publisher on pypi.org first — a silent regression that would 403 on the next publish.", + "ref": "https://github.com/ruvnet/RuView/pull/786" } ] }