Skip to content

feat(hu): make hu and its WASM plugins downloadable and usable - #301

Closed
YuanYuYuan wants to merge 21 commits into
mainfrom
feat/releasable-hu-plugins
Closed

feat(hu): make hu and its WASM plugins downloadable and usable#301
YuanYuYuan wants to merge 21 commits into
mainfrom
feat/releasable-hu-plugins

Conversation

@YuanYuYuan

@YuanYuYuan YuanYuYuan commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Note

Superseded. Closed without merging. This branch shipped as seven smaller pull requests, all merged. Its content is on main; nothing here is lost.

A user can now download hu and its WASM plugins and run them. No repo clone, and no build.

Where each part landed

PR What it delivered
#310 one version for every workspace crate
#312 the release artifact set, the packaging script, the installer, and hu plugin install / uninstall
#313 the coverage that would have caught the defects this work uncovered
#315 the WIT world pinned to its contract rather than to a copy of itself
#317 the hu target named in the default log filter
#318 axum 0.8 route syntax in hu web
#319 host I/O waits no longer charged to a plugin's compute budget
#320 the examples advertise their message types
#311 the message schema resolved before subscribe returns
#314 the bundled message definitions embedded

#297, #298 and #299 are closed by #311.

Why it was split

At 51 files and +4371 lines this mixed several deliverables, and review found defects in the slices that were invisible in the whole. Three examples, each found by reviewing one slice alone:

  • a guest-supplied timeout could switch off the host's preemption watchdog for about 49 days
  • hu plugin install from a signed URL wrote the credential into the installed filename
  • the release workflow called a script its own branch did not contain, so every v* tag would have failed and withdrawn its release

Breaking changes

Recorded on the pull request that carries each one: the hu meter echo exit status on #311, and the z_pubsub node names on #320.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ZettaScaleLabs.github.io/hiroz/pr-preview/pr-301/

Built to branch gh-pages at 2026-08-20 15:21 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

`cargo publish --workspace` reads each crate's version from its own manifest,
so four manifests carrying their own literal was four chances to publish a
release under a version the tag did not name.

Inherit from the workspace instead. The readers that parsed a crate manifest
for a version now read the workspace one.
A downloaded `hu` had no way to acquire a plugin: discovery reads a directory,
and nothing put anything in it. `hu plugin install` takes a local path, a URL,
or a name resolved against a release index, and `hu plugin uninstall` reverses
it.

Every install validates the component before it lands, so a file the host
cannot load is refused rather than left to fail at dispatch. A checksum
mismatch and a WIT world mismatch are both refusals, and the destination
filename is sanitised, so a name taken from a URL cannot escape the plugin
directory.

The subcommand a plugin provides comes from its filename, so the installer
strips the version a release asset carries. Without that, installing the
published `hu_meter-0.1.0.wasm` created `hu meter-0_1_0` and left the
documented `hu meter` nonexistent.
Three defects that only a shipped artifact could expose.

`hu web` panicked on startup. Its plugin routes used axum 0.7 wildcard syntax
against axum 0.8, and `Router::route` validates by panicking at run time, so it
compiled and died the moment anyone ran it. CI never compiled the feature.

The type-hash guard on the disk-schema path compared rendered strings. One
renderer is cfg-gated and collapses every hash to a single constant, so the
comparison could pass on anything. Compare values, and treat "no hash
advertised" as a third state rather than a mismatch, so a peer that advertises
none is not refused with advice no message definition can satisfy. Six tests
pin the decision.

The `hu-meter` plugin and the logger round out the set.
No release has ever contained a `.wasm`. The workflow built the `hu` binary
only, and the plugins were built by the test job, exercised, and discarded. A
user who downloaded a release got `No WASM plugins found`, and every `hu meter`
and `hu monitor` command failed to dispatch. RELEASING.md claimed otherwise.

`scripts/build-hu-release.nu` is now the single packaging step: it produces the
binary tarballs, both plugins, an offline plugins tarball, a machine-readable
index and a SHA256SUMS over the set, and it refuses to build when the tag and
the crate version disagree.

A tag is three different strings -- the release identity, the version the
filenames carry, and the version the binary reports. They coincide for a normal
release and diverge for a pre-release, which is why a pre-release is the only
tag shape that can catch this class of defect. test-release-version-semantics.sh
pins all three, and it needs no runner, no network and no build.

The release is created as a draft, promoted only once an install from the
published assets succeeds, and returned to draft if that verification fails.
`install-hu.sh` detects the platform, downloads the matching tarball, verifies
it against SHA256SUMS, and installs the binary and both plugins. `--offline`
does the same from a directory someone handed you, with no network.

A checksum mismatch and a missing SHA256SUMS entry are both refusals: an
unlisted file is exactly what a substituted file looks like. `curl --fail`
keeps an error page from being written to disk and unpacked as a binary. No
credential is embedded, and a token is read from the environment only.

22 assertions cover the refusals, since those are the paths that matter and the
ones a happy-path test never reaches.
The acceptance bar for this branch: a user who downloads `hu` can run what the
docs promise. The suite extracts every `hu` invocation from the tool docs and
runs it against an installed binary in a scratch HOME, with no cargo, no repo
on PATH and HU_PLUGIN_PATH unset.

A command is classified in the doc itself, next to the promise, so a reviewer
sees the reason. An unclassified command defaults to running, so the suite
cannot quietly degrade into testing nothing.

Running it is what found the defects fixed here, and the docs it corrects:
`hu web` does not bind 0.0.0.0, the `hu meter hz` sample output was wrong, and
a downloaded `hu` cannot publish at all, because message definitions do not
ship. The Quick Start now states that prerequisite instead of implying a
download is enough.
CI never compiled `web-plugins` and never ran `hiroz-union`'s tests. That is
why a run-time panic in `hu web` survived: the file was never compiled, let
alone executed. The crate already had 14 tests that had never run once --
`--lib` silently does nothing on a binary-only crate, so it needs `--bins`.

Add the docs-repro suite, an aarch64 cross-compile leg, and the release
dry-run, so the packaging path is exercised on a pull request instead of first
running on a tag.
The disk-schema fallback needs the pieces of the graph and dynamic-message API
that were previously internal.

The examples gain node names. They were "Pub" and "Sub", so the documented
output showing `/talker` and `/listener` could not be reproduced by running
them -- a docs promise the code did not keep.
@YuanYuYuan
YuanYuYuan force-pushed the feat/releasable-hu-plugins branch from cb031e2 to 53205d5 Compare August 20, 2026 08:54
build-binaries packages an aarch64-apple-darwin tarball and the release
job publishes it, but nothing ever unpacked or ran it: smoke-test-binaries
was a single ubuntu-latest job downloading bin-hu-x86_64-linux. A macOS
tarball that did not install, or a hu that did not start, would have
shipped with every check green.

Make the job a matrix over ubuntu-latest and macos-latest, each pulling its
own binary artifact and installing it with scripts/install-hu.sh. The
plugins artifact is wasm32-wasip2 and platform-independent, so both legs
consume the one produced by build-hu-plugins.

Passing no target makes install-hu.sh's detect_target pick the tarball,
so the macOS leg is also the first thing to exercise its Darwin/arm64 arm
against a real release layout. The corrupted-asset refusal now damages the
leg's own tarball; against a fixed target the macOS leg would have
installed an intact one and proved nothing.

Two spellings in the checksum step are Linux-only and had to go, or the
macOS leg would have failed before testing anything about the artifact:
find's GNU -printf, replaced by -exec basename, and sha256sum, which macOS
does not ship. The tool is now picked at run time between sha256sum and
shasum -a 256 — the same fallback install-hu.sh already makes, and the two
emit and verify an identical format.

aarch64-unknown-linux-gnu still has no leg: no GitHub-hosted runner can
execute it. That gap is now the only one, and it is visible in the matrix.
release.yml runs only on a `v*` tag, so the macOS smoke leg it now carries
cannot run here. Its first execution would be a real release, which is the
wrong place to find out that the macOS tarball does not install.

This job runs the same scripts on macos-latest: build-hu-release.nu packages a
native aarch64-apple-darwin build with the tag-vs-crate guard armed,
install-hu.sh installs it through the Darwin/arm64 path and the shasum
fallback, and the installed binary must run and discover both plugins. A
corrupted tarball must still be refused.

It is allowed to fail the run. A red job here is the finding.

REVERT BEFORE MERGE. The block is marked in the file.
…job)

The macOS smoke job failed packaging with "can't find crate for `core`" while
building the WASM plugins. The cause was a silently ignored input, not a
toolchain problem.

actions-rust-lang/setup-rust-toolchain takes `target`. It was given `targets`,
which it reports as an unexpected input with a WARNING and then ignores, so no
wasm target was ever installed. release.yml uses dtolnay/rust-toolchain, where
`targets` is the correct name. This job copied the action from ci.yml's macOS
legs, which need no wasm target, and the input name from release.yml.

release.yml itself is correct and is unaffected.

Still TEMPORARY. Revert before merge.
Both smoke-test-binaries legs failed on `cd: dist: No such file or directory`.
actions/checkout cleans the working directory, and it ran after the two
download steps, so it deleted the dist/ they had just populated.

The ordering was wrong from the day the job was written. Nothing could say so:
release.yml triggers only on a `v*` tag, and no tag had been cut from this
branch, so the job had never run. The v0.1.0-rc14 rehearsal is what surfaced
it, which is the reason to rehearse before merging rather than after.

Same shape as the apt cache key in #308: a step that depended on the checkout
ran before it and failed quietly in a way that read as something else.
Four gaps a first-time user hit, each closed at its source.

The installer demanded a version. Its own header promised "default: the
latest published" and the code died with "HU_VERSION is required" instead. It
now asks the release host which release is newest. /releases/latest excludes
drafts and pre-releases, so a bare install never lands on a rehearsal tag.

The default download URL pointed at a tag prefix this project does not use.
GitHub tags releases `v<version>`; the URL was built from `hu-v<version>`, a
prefix that belonged to a channel this branch no longer carries, so every
default install would have 404'd.

`hu plugin install <name>` refused until the user exported a registry URL --
for the plugins the running binary was released with. It now defaults to the
index published alongside its own version, pinned to CARGO_PKG_VERSION so a
plugin always matches its host.

`hu meter echo` subscribed and then printed nothing, which is correct on an
idle topic and indistinguishable from a broken tool. It now says what it is
waiting for, on stderr so piped output stays clean.

Tests: the suite gains the piped `sh -s --` invocation, which no test covered
-- every existing one runs the installer as a file, and piping differs in how
`$0` resolves and what consumes stdin. Breaking only that path turns exactly
the two new assertions red and leaves the other 22 green. 24 passed, 0 failed.

Docs updated to match: the quickest path no longer passes a version twice, and
the removed channel's tag shape is gone.
…RARY)

rc15 showed withdraw-release correctly staying skipped when verification
passes. That is half the claim. A job that never runs also never fires, so the
other half needs a failure to observe, and the condition reads this job's
result — nothing else can produce it.

The step is last, so every real assertion still runs and only the exit status
changes.

Expected on rc16: the release is created, promoted, then returned to draft by
withdraw-release. publish-crates stays skipped.

REVERT IMMEDIATELY AFTER THE RUN.
It existed because release.yml runs only on a `v*` tag, so the macOS smoke leg
could not be exercised by a pull request. The v0.1.0-rc15 and rc16 rehearsals
ran that leg for real, on both platforms, so this job now duplicates coverage
that the release pipeline provides.

What it proved while it existed: build-hu-release.nu packages on macOS,
install-hu.sh takes the Darwin/arm64 path and the shasum fallback, the
installed binary runs and discovers both plugins, and a corrupted macOS
tarball is refused. It also caught a typo'd action input -- `targets` given to
an action whose input is `target`, dropped with a warning rather than an error.
Comments were 30% of this branch's added lines, and release.yml carried the
most. Five blocks ran 12 to 18 lines each, restating history, prior defects
and rationale that the pull request and #309 already hold.

Two were not merely long. The draft/promote block began "derived from the tag
shape, mirroring" and then stopped mid-sentence: the scrub that removed the
second release channel took the rest of the line with it. And the installer
step stacked two comments that contradicted each other, the first describing a
piped invocation the code deliberately stopped using.

Each block now states what the line does and what breaks without it. 65 lines
out, 19 in.
Four blocks across three files, 12 to 31 lines each, restating defect history
the pull request and #309 already hold. Each now states what the code does and
what breaks without it.

One was damaged rather than merely long. build-hu-release.nu's header read
"Both / every release platform calls it, so they cannot drift / cannot drift
apart" -- the scrub that removed the second release channel cut a sentence in
half and left the remainder duplicated. That is the second such break found;
release.yml carried the first.

ros.rs 50 comment lines to 22, build-hu-release.nu 22 to 15,
test-hu-docs-repro.nu 40 to 29.
The humble interop leg failed to compile hiroz's build script:

    libstabby_macros-e78ef2ef160687eb.so: libc.so.6:
    version `GLIBC_2.39' not found

A cached proc-macro built against glibc 2.39 cannot load in the humble
container, which is 22.04 and ships 2.35. humble is the only 22.04 image in
the matrix.

Not transient. A rerun restored the same entry, reported "Cache restored
successfully", and failed on the same object hash. The control is the sibling
ROS Tests humble leg: same image, different cache key, passing. So the fault
is the entry, not the image and not the code — the three commits before this
one changed comments only.

Bumping shared-key forces one cold rebuild for this leg. ci.yml already
carries two -v2 keys, so this failure mode has been met before.
The humble leg could not compile hiroz's build script:

    libstabby_macros-...so: libc.so.6: version `GLIBC_2.39' not found

The cause is a second cache nobody declared. actions-rust-lang/setup-rust-toolchain
runs its own rust-cache unless told not to, and keys it on the JOB ID. That id
is `interop_test` for every distro in the matrix, so all four legs shared one
target/. humble is the only 22.04 image, so it restored proc-macros the 24.04
legs had built and failed to load them.

The log names it outright:

    Restored from cache key "v0-rust-interop_test-Linux-x64-..." full match: true

An earlier commit bumped the shared-key of the *explicit* cache below, which is
already per-distro and was never the problem. That did not help, and the leg
failed again with the same object hash.

`cache: false` leaves only the per-distro cache. The -v2 suffix stays, to
discard anything saved while the shared cache was in use.

Blast radius: the embedded key carries OS and arch but not the container image,
so only a job matrixed over images on one platform can collide. The other four
call sites matrix over ubuntu and macOS, which the key already separates, and
rmw-zenoh-rs.yml pins a single image.
There were two copies, and they had already drifted. The smoke test used a
run-time pick between sha256sum and shasum, and `find -exec basename`. The
release job used a bare `sha256sum` and GNU-only `find -printf`, so it would
fail on any host without coreutils. It works today only because that job runs
on ubuntu. Both carried the same "capture the list before the redirect"
warning, written twice in different words.

scripts/ci/write-sha256sums.sh is now the only implementation. It refuses a
missing directory, an empty one, and no argument, and it verifies what it
wrote. The release job keeps its two content assertions, which are not shared:
a release must cover a hu tarball and the installer.

Folding exposed a coverage hole. Replacing the assembly with `true` left the
detector green -- nothing asserted the asset set was checksummed at all. Two
assertions now pin it, and both were seen failing: deleting a call site turns
one red, reintroducing an inline `xargs sha256sum` turns the other red.
53 passed, 0 failed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant