diff --git a/.github/actions/headless-host/action.yml b/.github/actions/headless-host/action.yml index b6a8ba0..45b4ae0 100644 --- a/.github/actions/headless-host/action.yml +++ b/.github/actions/headless-host/action.yml @@ -1,12 +1,17 @@ name: Headless browser host description: > - Build chuzz-headless and install ps-qa, so a site's built output can be driven - through a real browser engine with no window. + Download the released chuzz-headless browser and install ps-qa, so a site's + built output can be driven through a real browser engine with no window. inputs: chuzz-ref: - description: The chuzz revision to build the host from. + description: The chuzz revision to build only when no released binary exists. default: master + chuzz-version: + description: > + Released browser version. Empty reads the version from the action's own + Cargo.toml, keeping callers on the binary released with this action. + default: "" token: description: > A token that can read pathscale/chuzz. Optional: chuzz is public, so the @@ -22,24 +27,65 @@ inputs: default: "" ps-qa-version: description: > - The driver's version requirement. A floor rather than "latest": a check - file can use a field an older driver does not know, and the failure then - reads as a broken workflow rather than a driver that is too old. - - 0.7.1 uses the shared control protocol and rejects checks that silently - inherit another file's page. Keep the driver on the same protocol line - as the headless host. - default: "^0.7.1" + The driver's compatible protocol line. Cargo installs the newest + published 0.7 release, keeping the driver and headless host on the same + protocol line without pinning a patch release. + default: "^0.7" outputs: host: - description: Path to the built chuzz-headless binary. - value: ${{ steps.build.outputs.host }} + description: Path to the verified chuzz-headless binary. + value: ${{ steps.acquire.outputs.host }} runs: using: composite steps: - - name: Check out chuzz + - name: Acquire released browser + id: acquire + shell: bash + env: + REQUESTED_VERSION: ${{ inputs.chuzz-version }} + ACTION_ROOT: ${{ github.action_path }}/../../.. + run: | + if [ "$(uname -s)" != "Linux" ] || [ "$(uname -m)" != "x86_64" ]; then + echo "::error::the released chuzz-headless host supports Linux x86_64" + exit 1 + fi + version="$REQUESTED_VERSION" + if [ -z "$version" ]; then + version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' "$ACTION_ROOT/Cargo.toml" | head -1) + fi + case "$version" in + ''|*[!0-9A-Za-z.+-]*) + echo "::error::invalid chuzz version '$version'" + exit 1 + ;; + esac + + asset=chuzz-headless-x86_64-unknown-linux-gnu.tar.gz + release=https://github.com/pathscale/chuzz/releases/download/headless-v$version + directory="$RUNNER_TEMP/chuzz-headless-$version" + mkdir -p "$directory" + + if curl -fsSL --retry 3 --retry-delay 2 "$release/$asset" -o "$directory/$asset" && \ + curl -fsSL --retry 3 --retry-delay 2 "$release/$asset.sha256" -o "$directory/$asset.sha256" && \ + (cd "$directory" && sha256sum -c "$asset.sha256"); then + tar -xzf "$directory/$asset" -C "$directory" + chmod +x "$directory/chuzz-headless" + if [ "$(cat "$directory/VERSION")" != "$version" ]; then + echo "::error::released browser version does not match $version" + exit 1 + fi + echo "released=true" >> "$GITHUB_OUTPUT" + echo "using released chuzz-headless $version" + else + echo "::warning::no verified chuzz-headless $version release; building ${{ inputs.chuzz-ref }} from source" + echo "released=false" >> "$GITHUB_OUTPUT" + fi + echo "host=$directory/chuzz-headless" >> "$GITHUB_OUTPUT" + + - name: Check out chuzz source fallback + if: steps.acquire.outputs.released != 'true' uses: actions/checkout@v4 with: repository: pathscale/chuzz @@ -56,26 +102,30 @@ runs: shell: bash run: | sudo apt-get update - sudo apt-get install -y pkg-config libfontconfig1-dev fonts-dejavu-core + packages=(fontconfig fonts-dejavu-core) + if [ "${{ steps.acquire.outputs.released }}" != "true" ]; then + packages+=(pkg-config libfontconfig1-dev) + fi + sudo apt-get install -y "${packages[@]}" - uses: Swatinem/rust-cache@v2 + if: steps.acquire.outputs.released != 'true' with: workspaces: .qa-host # A failing check must not make the corrective run rebuild a browser # engine from zero. cache-on-failure: true - # The driver links no renderer, so it installs from the registry. The host - # is built here because it is the browser, and the browser is not a crate a - # site can install. + # The driver links no renderer, so it installs from the registry. The + # browser is a versioned release asset built once by Chuzz's release job. - name: Install ps-qa shell: bash env: CARGO_TARGET_DIR: .qa-host/target/qa-tools run: cargo install ps-qa --version "${{ inputs.ps-qa-version }}" - - name: Build the host - id: build + - name: Build source fallback + if: steps.acquire.outputs.released != 'true' shell: bash run: | # `--no-default-features` drops `gui`, and with it `tauri`, `muda -> gtk` @@ -84,4 +134,4 @@ runs: cargo build --release --manifest-path .qa-host/Cargo.toml \ --bin chuzz-headless --no-default-features \ --features capture,javascript,scrollbars,webp,system-fonts - echo "host=$PWD/.qa-host/target/release/chuzz-headless" >> "$GITHUB_OUTPUT" + cp .qa-host/target/release/chuzz-headless "${{ steps.acquire.outputs.host }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33a3e9d..042acc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,52 @@ jobs: - name: Test run: cargo test -p chuzz-control --all-features + # The fleet's website QA consumes this exact feature set as a prebuilt Linux + # release asset. Build and drive it here once, in the repository that owns + # the browser, before a version can reach the release workflow. + headless: + runs-on: ubicloud-standard-4 + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Install native font dependencies + run: | + sudo apt-get update + sudo apt-get install -y pkg-config libfontconfig1-dev fonts-dejavu-core + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + with: + shared-key: chuzz-linux-headless-ci + cache-targets: true + + - name: Build and package the headless browser + run: scripts/package-headless.sh + + - name: Test the packaged feature set + run: | + cargo test --release -p chuzz \ + --no-default-features \ + --features capture,javascript,scrollbars,webp,system-fonts \ + --test serves_inspection + + - name: Verify the release archive + run: | + cd target/headless-release + sha256sum -c chuzz-headless-x86_64-unknown-linux-gnu.tar.gz.sha256 + tar -tzf chuzz-headless-x86_64-unknown-linux-gnu.tar.gz + + - name: Keep the candidate for review + uses: actions/upload-artifact@v4 + with: + name: chuzz-headless-x86_64-unknown-linux-gnu + path: target/headless-release/chuzz-headless-x86_64-unknown-linux-gnu.tar.gz* + if-no-files-found: error + retention-days: 7 + # The frontend, which is where most changes actually land. Independent of the # Rust jobs, so a TypeScript error does not wait on a cargo build. # @@ -121,12 +167,9 @@ jobs: - name: Test run: bun run test:run - # The app itself, on the platform it targets. This is the job that proves a - # change compiles against the pinned engine, which no Linux runner can do. - # - # `build-app.sh` is deliberately not used: bundling, signing and stamping are - # the release job's business. A PR only needs to know the code compiles and - # its tests pass. + # The app itself, on the platform it targets. The visible gate drives the + # same signed bundle a release would publish, including browser chrome, + # settings, theme controls, identity switching and the redraw stress page. app: runs-on: namespace-profile-agency-tahoe steps: @@ -159,3 +202,25 @@ jobs: - name: Test run: cargo test --workspace + + - name: Install the native QA harness + run: cargo install ps-qa --version '^0.7' + + - name: Build the review bundle + run: | + cargo build --release -p chuzz-qa-fixture + apps/chuzz/build-app.sh release + + - name: Drive the visible application + env: + QA_ARTIFACT_DIR: ${{ runner.temp }}/chuzz-visible-qa + run: scripts/qa-visible-app.sh + + - name: Keep visible QA evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: chuzz-visible-qa + path: ${{ runner.temp }}/chuzz-visible-qa + if-no-files-found: warn + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb6f0cc..3b84f4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,7 @@ jobs: outputs: version: ${{ steps.decide.outputs.version }} publish: ${{ steps.decide.outputs.publish }} + headless_publish: ${{ steps.decide.outputs.headless_publish }} steps: - name: Checkout uses: actions/checkout@v4 @@ -101,6 +102,73 @@ jobs: echo "Live is already $LIVE; nothing to do. Bump [workspace.package] version to release." fi + HEADLESS=https://github.com/pathscale/chuzz/releases/download/headless-v$VERSION/chuzz-headless-x86_64-unknown-linux-gnu.tar.gz + if [ "${{ inputs.force }}" = "true" ]; then + echo "headless_publish=true" >> "$GITHUB_OUTPUT" + elif curl -fsSIL --max-time 30 "$HEADLESS" >/dev/null 2>&1 && \ + curl -fsSIL --max-time 30 "$HEADLESS.sha256" >/dev/null 2>&1; then + echo "headless_publish=false" >> "$GITHUB_OUTPUT" + echo "The Linux headless asset for $VERSION is already published." + else + echo "headless_publish=true" >> "$GITHUB_OUTPUT" + echo "The Linux headless asset for $VERSION is missing; publishing it." + fi + + # Build the Linux QA browser once per Chuzz release. Fleet repositories use + # the checksum-verified asset through the composite headless-host action; + # they no longer each compile the same renderer and browser graph. + headless: + needs: check + if: needs.check.outputs.headless_publish == 'true' + runs-on: ubicloud-standard-4 + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Install native font dependencies + run: | + sudo apt-get update + sudo apt-get install -y pkg-config libfontconfig1-dev fonts-dejavu-core + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + with: + shared-key: chuzz-linux-headless-release + cache-targets: true + + - name: Build and package the headless browser + run: scripts/package-headless.sh + + - name: Test the packaged feature set + run: | + cargo test --release -p chuzz \ + --no-default-features \ + --features capture,javascript,scrollbars,webp,system-fonts \ + --test serves_inspection + + - name: Publish the versioned headless asset + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.check.outputs.version }} + run: | + tag="headless-v$VERSION" + if ! gh release view "$tag" >/dev/null 2>&1; then + gh release create "$tag" \ + --target "$GITHUB_SHA" \ + --title "Chuzz headless $VERSION" \ + --notes "Linux x86_64 native ps-qa host with capture, JavaScript, scrollbars, WebP, and system-font discovery." \ + --prerelease + fi + gh release upload "$tag" \ + target/headless-release/chuzz-headless-x86_64-unknown-linux-gnu.tar.gz \ + target/headless-release/chuzz-headless-x86_64-unknown-linux-gnu.tar.gz.sha256 \ + --clobber + release: needs: check if: needs.check.outputs.publish == 'true' @@ -171,6 +239,9 @@ jobs: with: targets: aarch64-apple-darwin + - name: Install the native QA harness + run: cargo install ps-qa --version '^0.7' + - name: Cache cargo build uses: Swatinem/rust-cache@v2 with: @@ -207,6 +278,22 @@ jobs: exit 1 fi + - name: Drive the exact release bundle + env: + QA_ARTIFACT_DIR: ${{ runner.temp }}/chuzz-visible-qa + run: | + cargo build --release -p chuzz-qa-fixture + scripts/qa-visible-app.sh "$BUNDLE_DIR/Chuzz.app" + + - name: Keep visible QA evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: chuzz-visible-qa-release-${{ needs.check.outputs.version }} + path: ${{ runner.temp }}/chuzz-visible-qa + if-no-files-found: warn + retention-days: 14 + # Tauri produces this tarball as a side effect of its bundler. Chuzz has no # Tauri, so it is packed here. COPYFILE_DISABLE stops bsdtar writing an # AppleDouble `._` sidecar beside every file carrying an extended diff --git a/.gitignore b/.gitignore index 039ce90..98ae50d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,6 @@ node_modules/ apps/chuzz/dist/ -# Handover notes are session state, tracked locally and never committed. -docs/HANDOVER-*.md - # Local-only renderer patch, see the file itself. .cargo/config.toml .cargo/config.toml.disabled @@ -22,6 +19,10 @@ apps/chuzz/gen/ # exist on one machine. See scripts/local-engine.sh. .cargo/local-engine.toml +# Handover and session notes are working state, not repository content. +HANDOVER*.md +docs/HANDOVER*.md + # 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 diff --git a/AGENTS.md b/AGENTS.md index 672ea10..4d06f63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,27 @@ - Work on a branch and ship through a pull request. Do not commit to `main`. - Run `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets --all-features -- -D warnings`, and `cargo test --workspace --all-features` before delivery. +## Invariants (do not break these) + +- **No Python.** Not a script, not `python3 -c`, not a heredoc. Do not swap it + for another ad hoc parser or assume `jq` is present: it does not ship with + macOS. Ask the tool that owns the answer for structured output. A fixed-shape + field can use one `sed -nE` line; logic that needs real parsing belongs in + this repository's Rust code, where it can be tested. + +## Handover documents are never committed + +Never commit a handover, status or session-summary document to this repository. +They are working notes for the owner and belong in the conversation. If one is +tracked, untrack it rather than editing it. + +## Merged is not fixed + +Only the owner closes a bug. A merged PR means the change shipped. Call it +finished only after the owner confirms it or after the exact reported path has +been driven on a running build. When that cannot be verified here, state what +remains unverified. + ## Git workflow - **Always specify the branch when pushing**: `git push origin branch-name` diff --git a/Cargo.toml b/Cargo.toml index 6a2a012..7b8b391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [workspace] -members = ["apps/chuzz", "crates/chuzz-control"] +members = ["apps/chuzz", "crates/chuzz-control", "crates/chuzz-qa-fixture"] resolver = "3" [workspace.package] -version = "0.1.38" +version = "0.1.39" edition = "2024" rust-version = "1.91" license = "MIT OR Apache-2.0" @@ -34,7 +34,7 @@ homepage = "https://github.com/pathscale/chuzz" # exercises what a release actually resolves. blitz-dom = { package = "ps-blitz-dom", version = "^0.4.9" } blitz-html = { package = "ps-blitz-html", version = "^0.4.9" } -blitz-net = { package = "ps-blitz-net", version = "^0.4.9" } +blitz-net = { package = "ps-blitz-net", version = "^0.4" } blitz-script = { package = "ps-blitz-script", version = "^0.4.9" } blitz-traits = { package = "ps-blitz-traits", version = "^0.4.9" } # The wasmi host binding over blitz-dom-api, so a WebAssembly guest can build a @@ -110,9 +110,14 @@ rustls = { version = "0.23", default-features = false, features = [ "aws-lc-rs", "std", ] } +cursor-icon = "^1" +cookie_store = "^0.22.1" +psl = "^2" futures-util = { version = "0.3", default-features = false, features = ["sink"] } +nagoya = "^0.1" tokio = "1" url = "2.5" +worktable = "^1.9.0-beta1" tauri = { version = "^2.11.5", default-features = false } # `diagnostics`, always, rather than behind a cargo feature. # diff --git a/README.md b/README.md index 935bcd7..160185f 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ Events are not wired up yet: the page renders and does not respond. ## Rendering without a window -For interactive website QA, build the headless host with fonts and use ps-qa -0.7.1 or newer: +For interactive website QA, build the headless host with fonts and use the +compatible ps-qa 0.7 line: ```sh cargo build --release --bin chuzz-headless --no-default-features \ @@ -106,10 +106,13 @@ ps-qa --app ../worktables.dev/tests/ps-qa/ps-qa.ron qa-hosted \ --checks ../worktables.dev/tests/ps-qa/checks ``` -Linux needs `pkg-config`, `libfontconfig1-dev`, and a font catalogue such as -`fonts-dejavu-core`; the shared headless-host CI action installs these. No desktop -server is needed. The `system-fonts` feature is optional for embedders supplying -their own fonts, but rendered website checks need real glyphs. +Linux source builds need `pkg-config`, `libfontconfig1-dev`, and a font catalogue +such as `fonts-dejavu-core`. The shared headless-host action normally downloads +the versioned Linux x86_64 release asset, verifies its published SHA-256, and +installs only the runtime font catalogue. If that asset is unavailable, the +action builds the same feature set from source. No desktop server is needed. The +`system-fonts` feature is optional for embedders supplying their own fonts, but +rendered website checks need real glyphs. The host dispatches input through the shared `DocumentControl` implementation, including pointer gestures, key-down/up, and scrolling. Once an action has been diff --git a/apps/chuzz/Cargo.toml b/apps/chuzz/Cargo.toml index 1865edb..9f1ed17 100644 --- a/apps/chuzz/Cargo.toml +++ b/apps/chuzz/Cargo.toml @@ -158,16 +158,24 @@ dioxus-native = { workspace = true, optional = true, features = [ "prelude", "svg", ] } -tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } +nagoya.workspace = true +# Tokio remains the I/O boundary for Tauri, HTTP, TCP and WebSocket. Portable +# timers use Nagoya, so this list names only the runtime/synchronisation surface +# those concrete I/O types require. +tokio = { workspace = true, features = ["macros", "rt", "sync"] } url.workspace = true serde.workspace = true serde_json.workspace = true tauri = { workspace = true, optional = true } rustls.workspace = true +cursor-icon.workspace = true +cookie_store.workspace = true +psl.workspace = true blitz-control-protocol = { workspace = true, features = ["engine", "server", "capture"] } tauri-runtime-blitz = { workspace = true, optional = true } tokio-tungstenite.workspace = true futures-util.workspace = true +worktable.workspace = true [dev-dependencies] # `flavor = "multi_thread"` in the script-fetch test: the blocking fetch takes @@ -182,7 +190,6 @@ tokio = { workspace = true, features = [ "rt", "rt-multi-thread", "sync", - "time", ] } # Assembles `fixtures/panel.wat` into a module in-process, so the capture test # owns its guest instead of borrowing blitz-wasm's demo, and so it needs no diff --git a/apps/chuzz/frontend/local-ui/bundle/package.json b/apps/chuzz/frontend/local-ui/bundle/package.json index b512770..db3b913 100644 --- a/apps/chuzz/frontend/local-ui/bundle/package.json +++ b/apps/chuzz/frontend/local-ui/bundle/package.json @@ -21,7 +21,7 @@ "**/*" ], "peerDependencies": { - "@pathscale/ui": "^3.1.0", + "@pathscale/ui": "^3.2.3", "solid-js": ">=2.0.0-rc.0", "solid-layouts": "^0.2.1" }, diff --git a/apps/chuzz/frontend/local-ui/package.json b/apps/chuzz/frontend/local-ui/package.json index 86cf80b..5b6cf68 100644 --- a/apps/chuzz/frontend/local-ui/package.json +++ b/apps/chuzz/frontend/local-ui/package.json @@ -9,7 +9,7 @@ "**/*.css" ], "peerDependencies": { - "@pathscale/ui": "^3.1.0", + "@pathscale/ui": "^3.2.3", "solid-js": ">=2.0.0-rc.0", "solid-layouts": "^0.2.1" } diff --git a/apps/chuzz/frontend/local-ui/src/components/panel-handle/PanelHandle.layout.tsx b/apps/chuzz/frontend/local-ui/src/components/panel-handle/PanelHandle.layout.tsx index 5610e5c..f1391e6 100644 --- a/apps/chuzz/frontend/local-ui/src/components/panel-handle/PanelHandle.layout.tsx +++ b/apps/chuzz/frontend/local-ui/src/components/panel-handle/PanelHandle.layout.tsx @@ -3,6 +3,7 @@ import type { Layout } from "solid-layouts"; import { panelHandle } from "./PanelHandle.recipe"; export type PanelHandleProps = { + id: string; title: string; collapsed?: boolean; onClick: JSX.EventHandlerUnion; @@ -18,6 +19,7 @@ export type PanelHandleProps = { */ const PanelHandle: Layout = () => ( + + + + + ); +} diff --git a/apps/chuzz/frontend/src/features/settings/DiagnosticsSection.tsx b/apps/chuzz/frontend/src/features/settings/DiagnosticsSection.tsx index f4179b2..e9d1a64 100644 --- a/apps/chuzz/frontend/src/features/settings/DiagnosticsSection.tsx +++ b/apps/chuzz/frontend/src/features/settings/DiagnosticsSection.tsx @@ -33,6 +33,7 @@ export function DiagnosticsSection(): JSX.Element {