From 94f3f042369d2dc148c0b3d886b512bb09efe6a8 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 9 Sep 2026 17:26:13 +0700 Subject: [PATCH 1/4] fix(release): read the version without a lockfile `cargo pkgid` resolves the dependency graph, so it requires a `Cargo.lock`. This repo stopped committing one, and the command has failed ever since: error: a Cargo.lock must exist for this command The step did not fail with it. `VERSION` was assigned the empty string from a failed substitution, the job continued, and the bundle verification compared `0.1.37` against nothing: ##[error]bundle reports 0.1.37 but this release is So 0.1.37 was merged, built and signed, and never published. `cargo metadata --no-deps` reads the workspace manifests without resolving anything, needs no lock, and creates none. Verified both ways with the lock moved aside: it returns 0.1.37 offline, while `pkgid` reproduces the error above. The empty-version guard is the other half. A release that cannot name itself should stop rather than publish under a blank version. --- .github/workflows/release.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0713b2..4c7fb29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,10 +62,23 @@ jobs: - name: Compare the committed version against the published one id: decide run: | - # `cargo pkgid` rather than a TOML parser. The version is resolved by - # the tool that owns it, so this cannot disagree with what the build - # actually produces, and it needs no interpreter on the runner. - VERSION=$(cargo pkgid -p chuzz-gui | sed 's/.*@//') + # Cargo reads the version rather than a TOML parser, so this cannot + # disagree with what the build produces and needs no interpreter on + # the runner. `--no-deps` reads the workspace manifests only. + # + # Not `cargo pkgid`: that resolves the dependency graph and so refuses + # to run without a `Cargo.lock`, which this repo deliberately does not + # commit. It exited non-zero here, left VERSION empty, and the job ran + # on to compare the built bundle against an empty string. 0.1.37 was + # merged, tagged and never published that way. Hence the guard below: + # an unreadable version fails the release instead of publishing a + # nameless one. + VERSION=$(cargo metadata --no-deps --format-version 1 \ + | sed -n 's/.*"name":"chuzz-gui","version":"\([^"]*\)".*/\1/p') + if [ -z "$VERSION" ]; then + echo "::error::could not read chuzz-gui's version from cargo metadata" + exit 1 + fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" # `latest.json` is written by this workflow further down, so its shape From 7fff90222525b4e31afc3c8e0e3a076f02b4ef02 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 9 Sep 2026 17:37:44 +0700 Subject: [PATCH 2/4] fix(build): do not build the browser chrome for a headless binary `build_frontend` ran unconditionally, so `cargo build --bin chuzz-headless --no-default-features` shelled out to `bun run build` in `apps/chuzz/frontend` for assets that binary never links. `frontend.rs` is the only consumer of the generated module and is already behind `gui`. On a machine without `apps/chuzz/frontend/node_modules` the build script panicked instead: error: script "layouts:local" exited with code 127 error: script "prebuild" exited with code 127 That is every runner using the `headless-host` action, which installs the site under test's dependencies and has no reason to install this crate's, so the whole fleet's QA went red in the host build step. Verified both ways with `node_modules` moved aside: the headless build now finishes, and `--bin chuzz-gui` still fails there with the error above, which is the proof the gate did it rather than something else. The helpers move behind the same feature so an unused import does not become a denied warning. --- apps/chuzz/build.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/chuzz/build.rs b/apps/chuzz/build.rs index 03f37e1..e5940b4 100644 --- a/apps/chuzz/build.rs +++ b/apps/chuzz/build.rs @@ -1,11 +1,21 @@ +use std::process::Command; + +// Everything below is the embedded browser chrome, which only a `gui` build +// compiles. Off that feature these are dead, and an unused import is a warning +// the workspace denies. +#[cfg(feature = "gui")] use std::fs; +#[cfg(feature = "gui")] use std::io::Write; +#[cfg(feature = "gui")] use std::path::{Path, PathBuf}; -use std::process::Command; +#[cfg(feature = "gui")] use brotli::CompressorWriter; +#[cfg(feature = "gui")] const CSS_MARKER: &str = "__CHUZZ_EMBEDDED_CSS__"; +#[cfg(feature = "gui")] const JS_URL: &str = "chuzz://ui/__chuzz__/app.js"; /// First line of a command's stdout, or `None` when it fails or prints nothing. @@ -51,6 +61,7 @@ fn stamp_build() { println!("cargo:rerun-if-changed=src"); } +#[cfg(feature = "gui")] fn only_file_with_extension(directory: &Path, extension: &str) -> PathBuf { let mut matches = fs::read_dir(directory) .unwrap_or_else(|error| panic!("cannot read {}: {error}", directory.display())) @@ -68,6 +79,7 @@ fn only_file_with_extension(directory: &Path, extension: &str) -> PathBuf { path } +#[cfg(feature = "gui")] fn compress_asset(path: &Path, output: &Path, quality: u32) -> usize { let input = fs::read(path).unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display())); @@ -84,6 +96,7 @@ fn compress_asset(path: &Path, output: &Path, quality: u32) -> usize { /// Compile and Brotli-embed the Solid browser chrome using the same asset /// loading shape as AgencyZero's Blitz document factory. +#[cfg(feature = "gui")] fn build_frontend() { let manifest_dir = PathBuf::from( std::env::var_os("CARGO_MANIFEST_DIR").expect("Cargo sets CARGO_MANIFEST_DIR"), @@ -170,6 +183,15 @@ fn strip_unused_frameworks() { fn main() { strip_unused_frameworks(); stamp_build(); + // The Solid browser chrome, consumed only by `frontend.rs`, which is itself + // behind `gui`. Building it unconditionally meant `cargo build --bin + // chuzz-headless --no-default-features` shelled out to `bun run build` for + // assets that binary never links, and then failed on any machine where + // `apps/chuzz/frontend/node_modules` was not installed. That is every CI + // runner using the headless-host action, which installs the site's + // dependencies and has no reason to install this crate's. It took the whole + // fleet's QA red. + #[cfg(feature = "gui")] build_frontend(); // Generates the Tauri context, which only the `chuzz-gui` binary consumes. // A headless build has no `tauri` in its graph for the context to describe, From 70e190cd796d11db62c912f3d879fae99c016221 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 9 Sep 2026 17:37:54 +0700 Subject: [PATCH 3/4] fix(tls): name the crypto provider instead of implying it `rustls` picks its provider from crate features and panics at the first handshake when the graph enables neither `ring` nor `aws-lc-rs`, or both. Features are additive across a graph, so which of those holds is an outcome of resolution rather than a decision anyone made, and with no lockfile it is not fixed at any point in time: one dependency picking up `ring` in a later release is enough to turn every `https://` fetch and every `wss://` connection into a panicked worker on the next runner that resolves it. It was hit on a fresh resolution during QA work, on `rustls 0.23.43`, and went away on re-resolution to 0.23.44. Today's graph enables `aws-lc-rs` alone, so this is latent rather than reproducible here, which is exactly the problem: nothing holds it there. The failure does not look like a browser failure. The socket never opens, Solid halts reactivity on the escaped error, and the page collapses to unnamed nodes, so a QA run reports a broken site. Both binaries now install a named provider before anything can reach the network. Verified by capturing an https page end to end. --- Cargo.toml | 8 ++++++++ apps/chuzz/Cargo.toml | 1 + apps/chuzz/src/headless_main.rs | 4 ++++ apps/chuzz/src/lib.rs | 22 ++++++++++++++++++++++ apps/chuzz/src/tauri_main.rs | 5 +++++ 5 files changed, 40 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 87791f8..a194a13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,6 +98,14 @@ tokio-tungstenite = { version = "0.30", default-features = false, features = [ "handshake", "rustls-tls-native-roots", ] } +# Named directly only so the binary can install a crypto provider by name. See +# `install_crypto_provider`: rustls otherwise picks one from crate features and +# panics at the first handshake when the resolution enables neither provider or +# both, and with no lockfile that is not fixed at any point in time. +rustls = { version = "0.23", default-features = false, features = [ + "aws-lc-rs", + "std", +] } futures-util = { version = "0.3", default-features = false, features = ["sink"] } tokio = "1" url = "2.5" diff --git a/apps/chuzz/Cargo.toml b/apps/chuzz/Cargo.toml index 790f0bc..9147919 100644 --- a/apps/chuzz/Cargo.toml +++ b/apps/chuzz/Cargo.toml @@ -151,6 +151,7 @@ url.workspace = true serde.workspace = true serde_json.workspace = true tauri = { workspace = true, optional = true } +rustls.workspace = true tauri-runtime-blitz.workspace = true tokio-tungstenite.workspace = true futures-util.workspace = true diff --git a/apps/chuzz/src/headless_main.rs b/apps/chuzz/src/headless_main.rs index bf50d0d..0ab4c17 100644 --- a/apps/chuzz/src/headless_main.rs +++ b/apps/chuzz/src/headless_main.rs @@ -14,6 +14,10 @@ use chuzz_gui::serve; fn main() { + // Before the loader can reach the network. See the function's own comment + // for why the provider is named here rather than left to the resolution. + chuzz_gui::install_crypto_provider(); + let args: Vec = std::env::args().collect(); let target = match serve::target_from(&args) { Ok(target) => target, diff --git a/apps/chuzz/src/lib.rs b/apps/chuzz/src/lib.rs index fa63d6e..7ce675c 100644 --- a/apps/chuzz/src/lib.rs +++ b/apps/chuzz/src/lib.rs @@ -14,6 +14,28 @@ //! the harness, so the harness measured a browser nobody ships. There is one //! loader now, and one place a gap gets fixed. +/// Name the TLS provider, rather than letting the resolver imply one. +/// +/// `rustls` selects its cryptographic provider from crate features, and panics +/// at the first handshake when the graph enables neither `ring` nor `aws-lc-rs` +/// or enables both. Features are additive across a dependency graph, so which +/// of those holds is an outcome of resolution rather than a decision anyone +/// made. This repository commits no lockfile, so it is not fixed at any point +/// in time either: one crate picking up `ring` in a later release is enough to +/// turn every `https://` fetch and every `wss://` connection into a panicked +/// worker on the next runner that resolves it. +/// +/// It presents as a site bug rather than a browser one. The socket never +/// opens, Solid halts reactivity on the escaped error, and the page collapses +/// to unnamed nodes, so a QA run reports a broken site. +/// +/// Both binaries call this before anything can reach the network. An `Err` +/// means a provider is already installed, which is the outcome being asked +/// for, so it is discarded. +pub fn install_crypto_provider() { + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); +} + // The window and its Tauri command surface. Behind `gui` because `tauri` is, // and because a headless build has no window to drive. #[cfg(feature = "gui")] diff --git a/apps/chuzz/src/tauri_main.rs b/apps/chuzz/src/tauri_main.rs index 21e2c6c..c9056ec 100644 --- a/apps/chuzz/src/tauri_main.rs +++ b/apps/chuzz/src/tauri_main.rs @@ -114,6 +114,11 @@ fn flag_value(args: &[String], flag: &str) -> Option { } fn main() { + // Before any of the entry points below can reach the network. See the + // function's own comment for why the provider is named here rather than + // left to the resolution. + chuzz_gui::install_crypto_provider(); + // Before `--capture`, and matched by equality rather than by prefix, so the // two flags cannot be confused for each other in either direction. #[cfg(feature = "capture")] From a053e9686578769f8f642d4bb791c2c58ad50fb1 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 9 Sep 2026 18:31:09 +0700 Subject: [PATCH 4/4] chore: drop the ignore rule for Python that is no longer here The corpus tooling was rewritten out of the repository, so nothing under scripts/corpus is Python any more and no build step produces bytecode. The ignore rule outlived the files it was written for. Leaving it in place is worse than merely dead. Python is not allowed in this tree, and an ignore rule for its bytecode is the one thing that would keep a reintroduction out of git status, so the rule quietly works against the convention it now has nothing to serve. --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 3326ed4..039ce90 100644 --- a/.gitignore +++ b/.gitignore @@ -22,10 +22,6 @@ apps/chuzz/gen/ # exist on one machine. See scripts/local-engine.sh. .cargo/local-engine.toml -# Python bytecode from scripts/corpus/ -__pycache__/ -*.pyc - # Lockfiles are not committed here. Every dependency is a caret range on a # published version, so a build resolves the newest thing that satisfies it and # a broken upstream release fails the build that introduced it. A committed lock