Skip to content

feat(hu): ship the plugins a release needs to be usable - #312

Merged
YuanYuYuan merged 7 commits into
mainfrom
feat/hu-release-pipeline
Aug 22, 2026
Merged

YuanYuYuan merged 7 commits into
mainfrom
feat/hu-release-pipeline

Conversation

@YuanYuYuan

@YuanYuYuan YuanYuYuan commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Replaces #301, together with the slices already merged. This now targets main.

The version-hygiene slice it used to stack on is merged as #310, so the sha2 dependency it needs is already in main.

#313 and #315 sit on this branch. Both retarget to main when it merges.

What this does

Makes a downloaded hu able to run its own plugin subcommands.

Component Role
scripts/build-hu-release.nu builds dist/. It is the single place that names and packages an asset, so a second release platform cannot drift from this one. Fails the build when the tag's core version disagrees with the crate
scripts/install-hu.sh POSIX installer. Verifies every download against SHA256SUMS and refuses a mismatch. --offline needs no network and no credential
hu plugin install / uninstall from a path, a URL, or the release index. Compiles the file as a component and refuses one that does not compile. Reuses the filename sanitiser, so a traversal name cannot escape the plugins directory. Checks the index's WIT world and schema version — only on the index path: a .wasm given as a path or a URL declares no world, so nothing checks one
Release workflow ships the plugins and the index. It creates the release as a draft, promotes it, and only then installs from the published assets — GitHub does not serve draft assets, so a download test cannot run before promotion. Any non-success from that verification returns the release to draft

What fails without this

No release has ever contained a .wasm file. On a machine that downloaded one:

$ hu plugin list
No WASM plugins found in $HU_PLUGIN_PATH or ~/.local/share/hu/plugins/.
$ hu meter hz /chatter
(no such subcommand - nothing to dispatch to)

RELEASING.md claimed the pipeline built hu_meter.wasm and hu_monitor.wasm. It did not.

Defects found by running the installed artifact

Every one lived in a path no test executed, and none would have failed a build.

Defect How it presented
A successful offline install exited 1 an EXIT trap ended in a falsy command under set -e
Checksum verification failed closed on valid downloads the lookup matched the filename literally, so SHA256SUMS written as ./name matched nothing
Installing hu_meter-0.1.0.wasm created the subcommand meter-0_1_0 discovery derives the name from the filename, so installing the release left hu meter nonexistent
A prefix install left discovery blind --prefix /opt/hu wrote plugins to /opt/hu/share/hu/plugins and discovery only looked under $HOME, so the install reported success and hu plugin list was empty
install-hu.sh was never published the install docs open by fetching it from the release. That URL returned 404 on every release ever cut

Breaking Changes

What changes Who is affected Before to after Action
The documented install one-liner readers of hu-install.md passed a version twice, to resolving the newest release itself none; pass --version to pin one

The existing v* release keeps its behaviour and gains the plugin assets, the index and the installer.

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-22 03:46 UTC

@YuanYuYuan
YuanYuYuan force-pushed the feat/hu-release-pipeline branch from feb9a5c to d4f172d Compare August 21, 2026 08:18
@YuanYuYuan
YuanYuYuan force-pushed the chore/hu-version-hygiene branch from 06b7336 to c7b3218 Compare August 21, 2026 08:18
@YuanYuYuan
YuanYuYuan changed the base branch from chore/hu-version-hygiene to main August 21, 2026 08:18
No release has ever contained a .wasm file, so a downloaded hu could not
run any documented `hu meter` or `hu monitor` command: plugin dispatch had
nothing to dispatch to. RELEASING.md claimed the pipeline built them.

Adds the artifact set (binary tarballs, both plugins, an offline plugins
tarball, a JSON index, the installer and SHA256SUMS), one packaging script
both release platforms call so they cannot drift, a POSIX installer that
verifies every download and refuses a mismatch, and `hu plugin install` /
`uninstall` from a path, a URL or the release index.

Discovery also learns a prefix-relative directory. Installing with a
--prefix wrote plugins where discovery never looked, so the install
reported success and `hu plugin list` was empty.
@YuanYuYuan
YuanYuYuan force-pushed the feat/hu-release-pipeline branch from d4f172d to 099c423 Compare August 21, 2026 19:34
F1 — the release workflow called a script this branch does not carry.

`smoke-test-release-install` ran `scripts/test-hu-docs-repro.nu`, which
the docs-coverage branch adds, not this one. Every `v*` tag would build,
promote the release, fail that step, withdraw the release back to draft
and skip crates.io. No release could complete. The workflow triggers on
tags only, so no pull request ever executed those lines.

The docs-reproduction step moves to the branch that owns the script, so
each lands with what it needs. The job keeps its two real checks: an
install from the published assets, and the documented installer URL
serving this tag's installer.

F2 — `hu plugin install` could never authenticate.

`http_get` appended `-H` after `--`. curl reads everything after `--` as
a URL, so the flag and the header became two more URL operands. Measured
with curl 8.21.0: exit 3 and `Could not resolve host: -H` with the header
after `--`, exit 0 with it before. Over http(s) curl also resolved a name
derived from the token.

Every URL and registry install failed whenever HU_RELEASE_TOKEN was set,
and the error blamed the download. No test set that variable, which is
why nothing caught it.

`curl_args` now builds the list, and three tests pin the order.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds complete hu release packaging, installation, plugin management, documentation, and release verification.

Changes:

  • Packages binaries, WASM plugins, registry indexes, checksums, and installer assets.
  • Adds plugin install/uninstall commands with validation and metadata.
  • Expands release automation, smoke tests, and installation documentation.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
.github/workflows/release.yml Builds, publishes, verifies, and withdraws releases.
Cargo.lock Records the checksum dependency.
RELEASING.md Updates release procedures.
crates/hiroz-union/Cargo.toml Adds sha2.
crates/hiroz-union/src/main.rs Exposes version and plugin management commands.
crates/hiroz-union/src/plugin/install.rs Implements plugin installation and removal.
crates/hiroz-union/src/plugin/mod.rs Exports the installer module.
crates/hiroz-union/src/plugin/wasm/mod.rs Adds prefix-relative discovery and shared helpers.
crates/hiroz-union/tests/plugin_install.rs Adds plugin refusal-path tests.
crates/hiroz-union/wit/v0.1/hu-plugin.wit Clarifies transport failures.
docs/tools/hu-install.md Documents installation workflows.
docs/tools/hu-plugins.md Documents plugin management.
mkdocs.yml Adds installation documentation navigation.
scripts/build-hu-release.nu Builds the release artifact set.
scripts/ci/write-sha256sums.sh Generates release checksums.
scripts/install-hu.sh Installs verified binaries and plugins.
Suppressed comments (5)

crates/hiroz-union/src/plugin/install.rs:348

  • name is inserted into a path without validation. For example, hu plugin uninstall /tmp/victim makes the third candidate /tmp/victim.wasm, so this command can delete a .wasm file outside the managed plugin directory. Reject any name changed by the existing sanitizer before constructing candidates.
pub fn uninstall(name: &str) -> Result<PathBuf> {
    let dir = install_dir()?;
    let candidates = [
        dir.join(format!("hu_{name}.wasm")),
        dir.join(format!("hu-{name}.wasm")),

crates/hiroz-union/src/plugin/install.rs:264

  • A direct URL install records the literal "url" as its plugin version, so both text and JSON listings expose url as if it were a release version. Keep the URL as provenance, but represent the unavailable version as absent rather than inventing a version value.
        return accept(&bytes, name, expected.as_deref(), source, "url");

RELEASING.md:50

  • The documented default behavior is not implemented: scripts/test-release-workflow.nu:18 still sets DEFAULT_TAG = "v0.0.0-smoke-test". With the new core-version guard, running the command shown above fails packaging as soon as the workspace version differs from 0.0.0. Update the script to derive the workspace version as claimed (and update its usage examples).
This pushes `v<crate-version>-smoke-test` — e.g. `v0.1.0-smoke-test` — waits for all CI jobs to pass (builds, smoke tests, release creation), then reports the result. The script requires `gh` CLI authenticated to the repo.

The tag carries the current workspace version deliberately. `build-hu-release.nu` cross-checks a tag's core version against it and fails the build on a mismatch, so a fixed tag like `v0.0.0-smoke-test` dies at the first packaging step. The `-smoke-test` suffix makes it a semver pre-release, so it publishes as a pre-release and skips the crates.io step.

RELEASING.md:19

  • This independent cadence is not supported by the workflow. It triggers on v*, strips only a leading v when deriving the crate version, and every non-prerelease run executes cargo publish --workspace; a hu-v* tag fails the core-version guard, while reusing v<workspace-version> cannot create a second release and would republish the same crates. Either implement a hu-specific tag path that skips workspace publication or remove this claim.
`hu` keeps an independent release *cadence* through its own `hu-v*` tags — you can cut a `hu` release between workspace releases — but not an independent *number*.

docs/tools/hu-install.md:68

  • SHA256SUMS now covers every release asset, so this command fails with “No such file” for all wheels, libraries, and other-platform archives a manual hu installer did not download. Show how to select and verify only the two downloaded tarball entries, or explicitly require downloading every listed asset.
Verify the download first. `sha256sum -c` exits non-zero on a mismatch, so stop here if it does — do not extract a file that failed this check:

<!-- repro: skip needs the release tarballs and their SHA256SUMS already downloaded -->
```bash
sha256sum -c SHA256SUMS
</details>



---

💡 <a href="/ZettaScaleLabs/hiroz/new/main?filename=.github/skills/code-review/SKILL.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add a `code-review` agent skill</a> or configure MCP servers for context-aware, tailored reviews. <a href="https://docs.github.com/en/copilot/how-tos/use-copilot-agents/request-a-code-review/use-code-review#mcp-servers-and-agent-skills" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn more in the docs.</a>

Comment thread scripts/ci/write-sha256sums.sh
Comment thread crates/hiroz-union/src/plugin/install.rs Outdated
Comment thread crates/hiroz-union/src/main.rs Outdated
Comment thread crates/hiroz-union/tests/plugin_install.rs
Comment thread crates/hiroz-union/tests/plugin_install.rs Outdated
Comment thread crates/hiroz-union/src/plugin/install.rs
Comment thread crates/hiroz-union/src/plugin/install.rs
Comment thread scripts/build-hu-release.nu Outdated
Comment thread crates/hiroz-union/src/plugin/install.rs Outdated
Comment thread crates/hiroz-union/src/plugin/install.rs Outdated
Metadata was recorded in the wrong fields. `accept` took the path or URL
as `source` and the literal "local"/"url" as `version`, so `hu plugin
list` printed `VERSION local` and `source_label` classified the entry by
sniffing a path. `source` now holds the kind, `version` is an `Option`
that only the registry index fills, and `origin` holds where it came
from with the userinfo and query string removed -- a signed asset URL
carries a token there, and `--json` prints it verbatim.

`hu plugin list` matched metadata by name alone. Discovery returns the
same name from `$HU_PLUGIN_PATH`, the executable-relative prefix and the
managed directory, so a development build was labelled with the
installed plugin's version. It now matches the managed file path too.

Other findings, each independent:

- An empty `HU_RELEASE_TOKEN` was treated as a credential and sent as
  `Authorization: token `, turning an anonymous public download into a
  401. The shell installer already treated empty as absent.
- The registry index declared a schema version that nothing checked, so
  `schema: 2` was read as schema 1 whenever the JSON still deserialised.
- `uninstall` discarded the result of persisting the install record. The
  file was already gone, so a failed write left an entry that the next
  install of that name would inherit, and the command reported success.
- The "installed elsewhere" error named `$HU_PLUGIN_PATH` even when the
  plugin sat in the executable-relative prefix and that variable was
  unset. It now names the directory that actually holds the file.
- `resolve_latest` sent no credential, so a private host answered 401
  before any authorised download was attempted. It also had no timeout.
- `HU_RELEASE_BASE` without a version asked this project's API for its
  newest release and named files after it. The installer now refuses
  that combination instead of 404ing against a correct directory.
- One handler served EXIT, INT and TERM. Returning 0 from a signal trap
  consumes the signal, so a Ctrl-C between two commands deleted the
  staging directories and let the install continue. The signal traps now
  exit.
- `withdraw-release` compared the verification result to 'failure'. A
  cancelled or skipped job reports neither, so an unverified release
  stayed public with the Latest badge. Any non-success now withdraws it.
- RELEASING.md promised an enforcement script that does not exist, and
  documented a `hu-v*` tag namespace that no workflow accepts. The
  installer's comments and its pre-release example said `hu-v` too.

Not changed: the `find -maxdepth` report. That is a BSD extension macOS
provides, and the script already avoids the GNU-only spellings.
Every test in this file was a refusal. No test had ever completed an
install and looked at the result, which is why the metadata defects in
the previous commit were found by review instead.

The obstacle was real but did not apply. `hiroz-union` cannot build a
plugin, because the plugins are a separate, excluded, wasm32-wasip2
workspace. `validate_plugin_static` only compiles the file as a
component, so an 8-byte empty component -- the magic, the component
version and the layer -- drives install, list and uninstall end to end.
It exports nothing and can never be dispatched, and no test claims it
can.

Adds five: a local install lands the file and records provenance, a URL
install verifies its sidecar, a signed URL's query string never reaches
installed.json, a registry install records the version the index states,
and uninstall removes the file and the record together.

`registry_install_without_a_registry_says_how_to_configure_one` removed
the registry override, so it fell back to the real GitHub index and made
an external request. It also would have started failing, rather than
erroring, once a release exists for this CARGO_PKG_VERSION. It now points
at a loopback port that serves 404.

Also corrects the module doc. It said the success paths lived at the end
of scripts/ci/hu-tests.sh. That script contains no `hu plugin install`
line, and grepping for one is how the claim was caught.
The tarball's README told the reader to run `sha256sum -c SHA256SUMS`.
That file covers every asset in the release, and a tarball holds only
some of them, so the command reports the absent ones as failures. It now
gives `--ignore-missing` and a single-file form for older coreutils.

Applies the ASD-STE100 sentence rules to the comments this branch adds.
Measured over the added comment lines: passive constructions 10 to 4,
sentences joining two independent clauses 1 to 0, longest 15 words.
`url_install_does_not_persist_a_credential_from_the_source_url` sent
`?token=SUPERSECRET`, and the fixture server routed on the whole request
target. The route table therefore missed and the server answered 404. It
now routes on the path, as a real asset host does with a query string it
does not recognise.

`uninstall_explains_when_the_plugin_lives_on_hu_plugin_path` asserted the
old wording. That message named `$HU_PLUGIN_PATH` unconditionally, which
is wrong when the plugin sits in the executable-relative prefix and the
variable is unset. The message now names the directory that holds the
file, so the test asserts on that directory and on the absence of the
"not installed" wording.
`hu plugin install <url>` derived the file name from the whole URL. A
signed asset URL carries its token in the query string, so the installed
file was named `hu_meter.wasm?token=...` -- the credential written into a
path on disk, where nothing redacts it. The previous commit stopped the
token reaching `installed.json`; it still reached the filesystem.

The sidecar URL was wrong for the same reason. Appending `.sha256` to the
full URL asks for `....wasm?token=....sha256`, which a host that ignores
unknown query parameters answers with the asset itself. The installer
then read the component bytes as a checksum and refused the install.

Both now use the URL's path, with the query re-attached for the sidecar
request. The credential test asserts the installed filename as well as
the record, because a filename is what the record fix did not cover.
@YuanYuYuan
YuanYuYuan merged commit 803705c into main Aug 22, 2026
32 checks passed
@YuanYuYuan
YuanYuYuan deleted the feat/hu-release-pipeline branch August 22, 2026 03:45
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.

2 participants