From bc1c78927e507f6ea2f8d83284b2b6db07182abe Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Tue, 28 Jul 2026 21:12:05 +0200 Subject: [PATCH] feat(oci): publish each cascade stage as its own cosign-signed OCI entity (jess#167 decision 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jess converged on the consumption contract (DD-026) and is waiting on the per-component publish so it can `wkg oci pull` each verified stage and fuse/lower/place per core — and measure the per-stage synth-skip inventory (which stage blocks first on the float gate, AFD-035). - release.yml: generalize the ghcr push from falcon-flight-only to a loop over flight + the five composable cascade stages (iekf/position/attitude/rate/ mixer). Each is pushed WASI-free (the build step already builds the bundle with --no-default-features) straight from the bundle dir (no re-download), and cosign-signed by immutable digest. Still continue-on-error (a registry hiccup must never fail the signed GitHub Release); per-component so a later hex airframe or a new stage is a one-line SLUGS edit, not a fork. - wasm/cm/{iekf,position,attitude,rate,falcon-mixer}/Cargo.toml: carry the full embedded metadata contract (description rewritten for an outside reader + keywords/documentation/categories/authors), so each component's wasm.directory listing is real, not a bare entry — same contract flight got in the prior PR. - docs/OCI-DISTRIBUTION.md: document the per-component publish set. Follow-ups: register each new component on wasm.directory (existing `pulseengine` namespace → auto-merge, not maintainer review) + make each ghcr package public. Takes effect on the next release; can be exercised on v1.128 via workflow_dispatch. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG --- .github/workflows/release.yml | 51 +++++++++++++++++++++------------ docs/OCI-DISTRIBUTION.md | 27 +++++++++++------ wasm/cm/attitude/Cargo.toml | 7 ++++- wasm/cm/falcon-mixer/Cargo.toml | 7 ++++- wasm/cm/iekf/Cargo.toml | 7 ++++- wasm/cm/position/Cargo.toml | 7 ++++- wasm/cm/rate/Cargo.toml | 7 ++++- 7 files changed, 80 insertions(+), 33 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f461cf8..599e8ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -527,7 +527,7 @@ jobs: # (continue-on-error): the primary artifact is the cosign-signed GitHub # Release above; a registry hiccup must never fail a release. Tighten to # hard-fail once proven over a few tags. Ref: ghcr.io//falcon-flight. - - name: Publish flight component to ghcr.io (OCI, cosign-signed) + - name: Publish cascade components to ghcr.io (OCI, cosign-signed) continue-on-error: true working-directory: relay env: @@ -546,28 +546,41 @@ jobs: fi MM=$(printf '%s' "$TAG" | sed -E 's/^falcon-v([0-9]+\.[0-9]+).*/\1/') FULL=$(printf '%s' "$TAG" | sed -E 's/^falcon-v//') - WASM="falcon-flight-v${MM}.wasm" - REPO="ghcr.io/${GITHUB_REPOSITORY_OWNER}/falcon-flight" - # Pull the exact wasm the release just attached (single source of truth). - gh release download "$TAG" -p "$WASM" --clobber + # Per-component OCI publish (jess#167 decision 5): each fine-grained + # verified cascade stage is its own cosign-signed OCI entity, so jess + # can `wkg oci pull` and fuse/lower/place per core. The bundle dir was + # produced WASI-free (--no-default-features) by the build step above; + # push straight from it (no re-download). `flight` is the runnable + # demo; the rest are the composable stages. + SLUGS="flight iekf position attitude rate mixer" + BUNDLE="dist/falcon-components-v${MM}" # wkg = the wasm-pkg-tools CLI. PINNED (supply-chain), bumped in # lockstep. Pushes the wasm-component OCI media type wasm.directory # expects — a plain `oras push` would tag it as a generic blob. cargo install wkg --locked --version 0.15.1 - echo "$GHCR_TOKEN" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin - wkg oci push "${REPO}:${FULL}" "$WASM" | tee push.log - # :latest is a moving convenience pointer — pushed for discovery, - # but NEVER signed (only immutable refs get a signature). - wkg oci push "${REPO}:latest" "$WASM" - - # Sign the IMMUTABLE digest, not a mutable tag. Only if we resolved one. - DIGEST=$(grep -oE 'sha256:[a-f0-9]{64}' push.log | head -1 || true) - if [ -n "$DIGEST" ]; then - cosign sign --yes "${REPO}@${DIGEST}" - echo "::notice::Published + signed ${REPO}@${DIGEST} (tags ${FULL}, latest). Pull: wkg oci pull ${REPO}:${FULL}" - else - echo "::warning::pushed ${REPO}:${FULL} but could not resolve a digest to sign — left unsigned this run" - fi + + published=0 + for slug in $SLUGS; do + WASM="${BUNDLE}/falcon-${slug}-v${MM}.wasm" + REPO="ghcr.io/${GITHUB_REPOSITORY_OWNER}/falcon-${slug}" + if [ ! -f "$WASM" ]; then + echo "::warning::missing ${WASM} — skipping falcon-${slug}"; continue + fi + wkg oci push "${REPO}:${FULL}" "$WASM" | tee "push-${slug}.log" + # :latest is a moving convenience pointer — pushed for discovery, + # but NEVER signed (only immutable refs get a signature). + wkg oci push "${REPO}:latest" "$WASM" + # Sign the IMMUTABLE digest, not a mutable tag. Only if we resolved one. + DIGEST=$(grep -oE 'sha256:[a-f0-9]{64}' "push-${slug}.log" | head -1 || true) + if [ -n "$DIGEST" ]; then + cosign sign --yes "${REPO}@${DIGEST}" + echo "::notice::Published + signed ${REPO}@${DIGEST} (tags ${FULL}, latest). Pull: wkg oci pull ${REPO}:${FULL}" + published=$((published + 1)) + else + echo "::warning::pushed ${REPO}:${FULL} but could not resolve a digest to sign — left unsigned this run" + fi + done + echo "::notice::per-component OCI publish: ${published} components signed + pushed" diff --git a/docs/OCI-DISTRIBUTION.md b/docs/OCI-DISTRIBUTION.md index 1ff989b..dcb34b1 100644 --- a/docs/OCI-DISTRIBUTION.md +++ b/docs/OCI-DISTRIBUTION.md @@ -8,19 +8,28 @@ a meta-registry that indexes OCI registries rather than hosting packages itself. ## What the release does automatically -`.github/workflows/release.yml` (flight-component job) pushes the exact wasm the -release attached: +`.github/workflows/release.yml` (flight-component job) publishes **each verified +cascade stage as its own cosign-signed OCI entity** (jess#167 decision 5) — the +fine-grained components jess `wkg oci pull`s and fuses/lowers/places per core: ``` -ghcr.io/pulseengine/falcon-flight: # e.g. 1.127.0 -ghcr.io/pulseengine/falcon-flight:latest +ghcr.io/pulseengine/falcon-flight: # runnable demo (sealed loop) +ghcr.io/pulseengine/falcon-iekf: # invariant-EKF estimator +ghcr.io/pulseengine/falcon-position: # position/velocity controller +ghcr.io/pulseengine/falcon-attitude: # geometric SO(3) attitude +ghcr.io/pulseengine/falcon-rate: # body-rate PID +ghcr.io/pulseengine/falcon-mixer: # control allocator +# ...each also tagged :latest ``` -pushed with `wkg` (wasm-pkg-tools) so it carries the Component-Model OCI media -type wasm.directory expects — not a generic blob — and cosign-signed keyless, -like the release bundle. The step is **non-blocking** (`continue-on-error`): a -registry hiccup must never fail the primary signed GitHub Release. It will be -tightened to a hard failure once it has proven itself across a few tags. +Each is built **WASI-free** (`--no-default-features`, so it lowers to bare metal +— see `wasm/cm/*`) and pushed with `wkg` (wasm-pkg-tools) so it carries the +Component-Model OCI media type wasm.directory expects — not a generic blob — and +cosign-signed keyless by immutable digest, like the release bundle. The step is +**non-blocking** (`continue-on-error`): a registry hiccup must never fail the +primary signed GitHub Release. It will be tightened to a hard failure once it has +proven itself across a few tags. Every published component carries the full +embedded metadata contract (below) so its wasm.directory listing is real. Pull + verify: diff --git a/wasm/cm/attitude/Cargo.toml b/wasm/cm/attitude/Cargo.toml index 89e1c33..8c8bd42 100644 --- a/wasm/cm/attitude/Cargo.toml +++ b/wasm/cm/attitude/Cargo.toml @@ -2,7 +2,12 @@ # workspace (generated bindings module won't build under plain cargo). [package] name = "falcon-attitude-cm" -description = "Falcon v0.7 — attitude as a Component Model component (wac cascade)" +description = "Formally-verified geometric SO(3) attitude controller as a WebAssembly Component — attitude setpoint + vehicle state → body-rate setpoint." +homepage = "https://pulseengine.eu" +documentation = "https://github.com/pulseengine/relay/blob/main/docs/OCI-DISTRIBUTION.md" +authors = ["PulseEngine"] +keywords = ["flight-control", "verified", "drone", "embedded", "wasm"] +categories = ["aerospace", "science::robotics"] version = "0.1.0" edition = "2024" license = "Apache-2.0" diff --git a/wasm/cm/falcon-mixer/Cargo.toml b/wasm/cm/falcon-mixer/Cargo.toml index 7ef413b..eb74ea9 100644 --- a/wasm/cm/falcon-mixer/Cargo.toml +++ b/wasm/cm/falcon-mixer/Cargo.toml @@ -4,7 +4,12 @@ # via `cargo component build`. [package] name = "falcon-mixer-cm" -description = "Falcon v0.7 — mixer as a Component Model component (wac cascade)" +description = "Formally-verified control allocator (mixer) as a WebAssembly Component — body torque + thrust → per-motor commands (airframe-parameterizable)." +homepage = "https://pulseengine.eu" +documentation = "https://github.com/pulseengine/relay/blob/main/docs/OCI-DISTRIBUTION.md" +authors = ["PulseEngine"] +keywords = ["flight-control", "verified", "drone", "embedded", "wasm"] +categories = ["aerospace", "science::robotics"] version = "0.1.0" edition = "2024" license = "Apache-2.0" diff --git a/wasm/cm/iekf/Cargo.toml b/wasm/cm/iekf/Cargo.toml index bf3ae52..00d3b3c 100644 --- a/wasm/cm/iekf/Cargo.toml +++ b/wasm/cm/iekf/Cargo.toml @@ -2,7 +2,12 @@ # workspace (generated bindings module won't build under plain cargo). [package] name = "falcon-iekf-cm" -description = "Falcon v1.4 — IEKF (SE₂(3)) as a Component Model component, exporting the falcon:cascade/ekf interface" +description = "Formally-verified invariant-EKF (SE₂(3)) navigation estimator as a WebAssembly Component — fuses IMU/GNSS/mag/baro into a 16-state attitude/velocity/position/bias estimate." +homepage = "https://pulseengine.eu" +documentation = "https://github.com/pulseengine/relay/blob/main/docs/OCI-DISTRIBUTION.md" +authors = ["PulseEngine"] +keywords = ["flight-control", "verified", "drone", "embedded", "wasm"] +categories = ["aerospace", "science::robotics"] version = "0.1.0" edition = "2024" license = "Apache-2.0" diff --git a/wasm/cm/position/Cargo.toml b/wasm/cm/position/Cargo.toml index 107d8e9..41a2ad7 100644 --- a/wasm/cm/position/Cargo.toml +++ b/wasm/cm/position/Cargo.toml @@ -2,7 +2,12 @@ # workspace (generated bindings module won't build under plain cargo). [package] name = "falcon-position-cm" -description = "Falcon v0.7 — position as a Component Model component (wac cascade)" +description = "Formally-verified position/velocity controller as a WebAssembly Component — NED waypoint + vehicle state → attitude setpoint." +homepage = "https://pulseengine.eu" +documentation = "https://github.com/pulseengine/relay/blob/main/docs/OCI-DISTRIBUTION.md" +authors = ["PulseEngine"] +keywords = ["flight-control", "verified", "drone", "embedded", "wasm"] +categories = ["aerospace", "science::robotics"] version = "0.1.0" edition = "2024" license = "Apache-2.0" diff --git a/wasm/cm/rate/Cargo.toml b/wasm/cm/rate/Cargo.toml index 22f521c..1d67dc0 100644 --- a/wasm/cm/rate/Cargo.toml +++ b/wasm/cm/rate/Cargo.toml @@ -2,7 +2,12 @@ # workspace (generated bindings module won't build under plain cargo). [package] name = "falcon-rate-cm" -description = "Falcon v0.7 — rate as a Component Model component (wac cascade)" +description = "Formally-verified body-rate PID controller as a WebAssembly Component — body-rate setpoint + gyro → body torque. Airframe-agnostic; part of the falcon:cascade control loop." +homepage = "https://pulseengine.eu" +documentation = "https://github.com/pulseengine/relay/blob/main/docs/OCI-DISTRIBUTION.md" +authors = ["PulseEngine"] +keywords = ["flight-control", "verified", "drone", "embedded", "wasm"] +categories = ["aerospace", "science::robotics"] version = "0.1.0" edition = "2024" license = "Apache-2.0"