From 0b47de3503610d5f73926f62e3f17c8e915a8149 Mon Sep 17 00:00:00 2001 From: reviewer Date: Sat, 17 Oct 2026 21:17:22 +0800 Subject: [PATCH 1/4] [CTX-1283] fix(runtime): preserve WSL scratch paths and help bytes --- crates/bitty-runtime/src/component/wsl.rs | 4 +++ crates/bitty-terminal/src/component.rs | 39 ++++++++++++++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/crates/bitty-runtime/src/component/wsl.rs b/crates/bitty-runtime/src/component/wsl.rs index 6cc480850..fc821a36e 100644 --- a/crates/bitty-runtime/src/component/wsl.rs +++ b/crates/bitty-runtime/src/component/wsl.rs @@ -351,9 +351,11 @@ pub fn windows_path_to_guest(windows: &str) -> Result { { let drive = bytes[0].to_ascii_lowercase() as char; let remainder = windows[3..].replace('\\', "/"); + // scratch-paths-exempt: intentional WSL guest contract path, not a host path. let guest = if remainder.is_empty() { format!("/mnt/{drive}") } else { + // scratch-paths-exempt: intentional WSL guest contract path, not a host path. format!("/mnt/{drive}/{remainder}") }; if guest.len() > WSL_PATH_MAX_BYTES { @@ -385,6 +387,7 @@ pub fn guest_mnt_to_windows(guest: &str) -> Result { "unmappable guest path {guest:?} (want 1..={WSL_PATH_MAX_BYTES} bytes, no NUL or control characters)" ))); } + // scratch-paths-exempt: intentional WSL guest contract path, not a host path. let Some(rest) = guest.strip_prefix("/mnt/") else { return Err(WslError::UnmappablePath(format!( "unmappable guest path {guest:?} (only /mnt//... has a Windows spelling; use the Windows-to-guest direction for spawn)" @@ -394,6 +397,7 @@ pub fn guest_mnt_to_windows(guest: &str) -> Result { let drive = parts.next().unwrap_or(""); let remainder = parts.next().unwrap_or(""); if drive.len() != 1 || !drive.bytes().all(|b| b.is_ascii_lowercase()) { + // scratch-paths-exempt: intentional WSL guest contract path, not a host path. return Err(WslError::UnmappablePath(format!( "unmappable guest path {guest:?} (want /mnt//...)" ))); diff --git a/crates/bitty-terminal/src/component.rs b/crates/bitty-terminal/src/component.rs index 4c174700f..89ccabbc3 100644 --- a/crates/bitty-terminal/src/component.rs +++ b/crates/bitty-terminal/src/component.rs @@ -804,7 +804,8 @@ pub fn component_usage() -> String { /// Long help (`bitty extension --help`). #[must_use] pub fn component_help_text() -> String { - "bitty extension — CLI-first native binary extension management (local class, no execution)\n\ + concat!( + "bitty extension — CLI-first native binary extension management (local class, no execution)\n\ \n\ usage: bitty extension [args] [flags]\n\ \n\ @@ -891,14 +892,16 @@ pub fn component_help_text() -> String { \x20 distro (no --distribution flag). A selected distro with absent WSL\n\ \x20 fails closed before any staging (exit 1): install WSL2 from\n\ \x20 the Store (inbox builds use --status fallback, unversioned) or run\n\ - \x20 without --distro. Path mapping (Windows to guest for spawn):\n\ - \x20 drive-absolute X:\\... maps to /mnt//... and WSL UNC\n\ + \x20 without --distro. Path mapping (Windows to guest for spawn):\n", + // scratch-paths-exempt: intentional WSL help contract path example, not a host path. + "\x20drive-absolute X:\\... maps to /mnt//... and WSL UNC\n\ \x20 \\\\wsl$\\\\... maps to guest-absolute /...; UNC paths bind\n\ \x20 to their launch distro (cross-distro exec fails closed); device\n\ \x20 paths, drive-relative, relative, and other UNC spellings fail closed\n\ \x20 (exit 2, no silent mangling). Backslash rewriting happens only inside\n\ - \x20 path mapping; general argv elements pass through discrete with spaces\n\ - \x20 preserved and metacharacters inert. Reverse (/mnt/... to Windows)\n\ + \x20 path mapping; general argv elements pass through discrete with spaces\n", + // scratch-paths-exempt: intentional WSL help contract path example, not a host path. + "\x20preserved and metacharacters inert. Reverse (/mnt/... to Windows)\n\ \x20 exists for diagnostics only; other guest paths have no Windows\n\ \x20 spelling. Guest env is WSLENV-gated (forwarded proxy/TLS/locale\n\ \x20 names only, secrets never, otherwise clean by design). Stdio and\n\ @@ -987,6 +990,7 @@ pub fn component_help_text() -> String { \x20 bitty extension remove net 0.0.1\n\ \x20 bitty extension show-source net\n\ \x20 bitty extension show-source net 0.0.1" + ) .to_string() } @@ -3364,6 +3368,31 @@ mod tests { let _ = std::fs::remove_dir_all(&base); } + #[test] + fn help_path_mapping_lines_keep_original_bytes() { + let help = component_help_text(); + let lines: Vec<&[u8]> = help + .split_inclusive('\n') + .filter(|line| { + line.contains("drive-absolute X:") + || line.contains("\\\\wsl$\\") + || line.contains("preserved and metacharacters inert.") + }) + .map(str::as_bytes) + .collect(); + + assert_eq!( + lines, + vec![ + b"\x20drive-absolute X:\\... maps to /mnt//... and WSL UNC\n".as_slice(), + b"\x20\x20\\\\wsl$\\\\... maps to guest-absolute /...; UNC paths bind\n" + .as_slice(), + b"\x20preserved and metacharacters inert. Reverse (/mnt/... to Windows)\n" + .as_slice(), + ] + ); + } + #[test] fn list_is_empty_without_roots_or_components() { let base = scratch("empty"); From ec4dab84e3b683599937e038750b0de0fcc2cd52 Mon Sep 17 00:00:00 2001 From: reviewer Date: Sat, 17 Oct 2026 22:28:39 +0800 Subject: [PATCH 2/4] [CTX-1306] fix(cli): preserve exact parent WSL help bytes Restore the two leading spaces verified by compiling the exact unmodified parent help function. Correct the byte regression assertions and retain narrow WSL scratch-path exemptions. Task: CTX-1306 Refs #2200 --- crates/bitty-terminal/src/component.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bitty-terminal/src/component.rs b/crates/bitty-terminal/src/component.rs index 89ccabbc3..dfc2cf710 100644 --- a/crates/bitty-terminal/src/component.rs +++ b/crates/bitty-terminal/src/component.rs @@ -894,14 +894,14 @@ pub fn component_help_text() -> String { \x20 the Store (inbox builds use --status fallback, unversioned) or run\n\ \x20 without --distro. Path mapping (Windows to guest for spawn):\n", // scratch-paths-exempt: intentional WSL help contract path example, not a host path. - "\x20drive-absolute X:\\... maps to /mnt//... and WSL UNC\n\ + "\x20 drive-absolute X:\\... maps to /mnt//... and WSL UNC\n\ \x20 \\\\wsl$\\\\... maps to guest-absolute /...; UNC paths bind\n\ \x20 to their launch distro (cross-distro exec fails closed); device\n\ \x20 paths, drive-relative, relative, and other UNC spellings fail closed\n\ \x20 (exit 2, no silent mangling). Backslash rewriting happens only inside\n\ \x20 path mapping; general argv elements pass through discrete with spaces\n", // scratch-paths-exempt: intentional WSL help contract path example, not a host path. - "\x20preserved and metacharacters inert. Reverse (/mnt/... to Windows)\n\ + "\x20 preserved and metacharacters inert. Reverse (/mnt/... to Windows)\n\ \x20 exists for diagnostics only; other guest paths have no Windows\n\ \x20 spelling. Guest env is WSLENV-gated (forwarded proxy/TLS/locale\n\ \x20 names only, secrets never, otherwise clean by design). Stdio and\n\ @@ -3384,10 +3384,11 @@ mod tests { assert_eq!( lines, vec![ - b"\x20drive-absolute X:\\... maps to /mnt//... and WSL UNC\n".as_slice(), + b"\x20\x20drive-absolute X:\\... maps to /mnt//... and WSL UNC\n" + .as_slice(), b"\x20\x20\\\\wsl$\\\\... maps to guest-absolute /...; UNC paths bind\n" .as_slice(), - b"\x20preserved and metacharacters inert. Reverse (/mnt/... to Windows)\n" + b"\x20\x20preserved and metacharacters inert. Reverse (/mnt/... to Windows)\n" .as_slice(), ] ); From 60591e68e3f8af8329d4205b3cf5a486dfeb4978 Mon Sep 17 00:00:00 2001 From: reviewer Date: Sat, 17 Oct 2026 22:34:37 +0800 Subject: [PATCH 3/4] [CTX-1311] fix(sandbox): document inert ELF parser fixture Explain the no-execution fixture using the existing per-hit PTY gate exemption. Preserve Linux cfg, executable tokens, and global gate behavior. Task: CTX-1311 Refs #2203 --- crates/bitty-sandbox/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitty-sandbox/src/lib.rs b/crates/bitty-sandbox/src/lib.rs index 27c1d0ebf..95a496024 100644 --- a/crates/bitty-sandbox/src/lib.rs +++ b/crates/bitty-sandbox/src/lib.rs @@ -673,6 +673,7 @@ mod tests { )); std::fs::create_dir_all(&dir).expect("temp dir must build"); let script = dir.join("probe.sh"); + // pty-gate-exempt: inert script fixture for elf_interp; never executed. std::fs::write(&script, "#!/bin/sh\nexit 0\n").expect("fixture must write"); assert_eq!(linux::elf_interp(&script), None); let _ = std::fs::remove_dir_all(&dir); From a38cbe7d943b314ed583d16e1737ecb6eb93aacc Mon Sep 17 00:00:00 2001 From: reviewer Date: Sat, 17 Oct 2026 22:42:37 +0800 Subject: [PATCH 4/4] [CTX-1308] fix(ci): run local Quality shards with disk sccache Map the current Ubuntu runner, use bounded local disk caching under act while preserving remote GHA caching, and serialize bind-mounted matrix checkouts. Bind the local cache endpoint to loopback and cover dispatch, backend setup, locks, and failure propagation. Refs #2201 --- .github/act/Dockerfile | 2 +- .github/workflows/ci.yml | 18 +++++ justfile | 27 ++++--- scripts/tests/ci-local.test.sh | 124 +++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 scripts/tests/ci-local.test.sh diff --git a/.github/act/Dockerfile b/.github/act/Dockerfile index 92c58e204..538e56249 100644 --- a/.github/act/Dockerfile +++ b/.github/act/Dockerfile @@ -14,7 +14,7 @@ # Usage (real local CI, e.g. the Quality gates job): # act -W .github/workflows/ci.yml -j quality \ # --container-architecture linux/amd64 \ -# -P ubuntu-latest=bitty-act:latest +# -P ubuntu-24.04=bitty-act:latest # # Keep the Rust channel in sync with rust-toolchain.toml when it changes. FROM catthehacker/ubuntu:act-latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4b79d57f..c41c85f95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,6 +121,20 @@ jobs: toolchain: ${{ steps.rust-channel.outputs.channel }} components: rustfmt, clippy + # act lacks the Actions runtime cache credentials. Keep the compiler + # wrapper and use bounded disk caches under the persisted target mount; + # each shard owns a directory because disk caches support one server. + # A nonempty GHA namespace enables that backend even when disabled. + - name: Configure local sccache backend + if: env.ACT + run: | + { + echo "SCCACHE_GHA_ENABLED=false" + echo "SCCACHE_GHA_VERSION=" + echo "SCCACHE_DIR=$CARGO_TARGET_DIR/sccache-${{ matrix.shard }}" + echo "SCCACHE_CACHE_SIZE=1G" + } >> "$GITHUB_ENV" + # CTX-0971 R4: sccache server on the GHA backend (shares dep units with # the linux-x11/linux-wayland jobs via the same SCCACHE_GHA_VERSION # namespace). Kept alongside rust-cache below; posts stats automatically. @@ -140,6 +154,10 @@ jobs: # CTX-0971 R4 sharding: lint/check steps run once (shard 1) so the two # shards do not duplicate clippy/fmt/packaging work. Test steps below # are partitioned or split across both shards. + - name: Check local Quality dispatch and cache setup + if: matrix.shard == 1 + run: bash scripts/tests/ci-local.test.sh + - name: Check Rust formatting if: matrix.shard == 1 run: cargo fmt --all -- --check diff --git a/justfile b/justfile index 1fa065d98..e115cc5e1 100644 --- a/justfile +++ b/justfile @@ -127,14 +127,23 @@ ci-local *args: # Bind the persistent target dir at both CARGO_TARGET_DIR and the repo's # own target/ so gates that read target/debug/bitty see a container-built # binary instead of the host's (the host glibc is newer than the image's). - exec act -W .github/workflows/ci.yml -j quality \ - --pull=false --bind --container-architecture linux/amd64 \ - -P ubuntu-latest=bitty-act:latest \ - --container-options "-u ubuntu ${group_add[*]:-} -v $cache/cargo-registry:$ctr_cargo/registry -v $cache/cargo-git:$ctr_cargo/git -v $cache/target:/cache/target -v $cache/target:$root/target" \ - --env HOME=$ctr_home --env CARGO_HOME=$ctr_cargo --env RUSTUP_HOME=$ctr_rustup \ - --env CARGO_TARGET_DIR=/cache/target --env CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-$(nproc)}" \ - ${proxy_env[@]+"${proxy_env[@]}"} \ - {{args}} + # Matrix entries share this bind mount, including external suite checkouts. + # act's --concurrent-jobs does not serialize entries of one matrix job; + # run each shard explicitly under the same repository lock instead. + for shard in 1 2; do + act -W .github/workflows/ci.yml -j quality --matrix "shard:$shard" \ + --cache-server-addr 127.0.0.1 \ + --pull=false --bind --container-architecture linux/amd64 \ + -P ubuntu-24.04=bitty-act:latest \ + --container-options "-u ubuntu ${group_add[*]:-} -v $cache/cargo-registry:$ctr_cargo/registry -v $cache/cargo-git:$ctr_cargo/git -v $cache/target:/cache/target -v $cache/target:$root/target" \ + --env HOME=$ctr_home --env CARGO_HOME=$ctr_cargo --env RUSTUP_HOME=$ctr_rustup \ + --env CARGO_TARGET_DIR=/cache/target --env CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-$(nproc)}" \ + ${proxy_env[@]+"${proxy_env[@]}"} \ + {{args}} + done + +ci-local-test: + bash scripts/tests/ci-local.test.sh pty-gate: ./scripts/check-pty-gated-tests.sh @@ -313,7 +322,7 @@ commit-check message: @cp commitlint.config.ts target/dev-tools/commitlint.config.ts @msg="$(realpath "{{message}}")" && cd target/dev-tools && bunx --bun commitlint --edit "$msg" -check: fmt-check clippy test supply-chain supply-chain-test scratch-paths scratch-paths-test pty-gate test-support-gate status-drift status-drift-test docs-pin-test runtime-deps-test terminfo-check terminfo-test desktop-check desktop-test install-smoke-test install-scripts-test unix-bundle-test component-dist-test macos-dmg-test windows-installer-test verify-unix-bundle-dispatch-test rust-channel-test binary-arch-test workflow-publish-test m1-matrix-test compat-matrix-test compat-freshness-test real-render-soak-test dogfood-session-test actionlint markdownlint +check: fmt-check clippy test supply-chain supply-chain-test scratch-paths scratch-paths-test pty-gate test-support-gate status-drift status-drift-test docs-pin-test runtime-deps-test terminfo-check terminfo-test desktop-check desktop-test install-smoke-test install-scripts-test unix-bundle-test component-dist-test macos-dmg-test windows-installer-test verify-unix-bundle-dispatch-test rust-channel-test binary-arch-test workflow-publish-test m1-matrix-test compat-matrix-test compat-freshness-test real-render-soak-test dogfood-session-test ci-local-test actionlint markdownlint # Parser-throughput baseline (CTX-0576, M1-11). Runs the deterministic # headless parser benchmark over the committed VT/escape corpora and verifies diff --git a/scripts/tests/ci-local.test.sh b/scripts/tests/ci-local.test.sh new file mode 100644 index 000000000..4807f899d --- /dev/null +++ b/scripts/tests/ci-local.test.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# ci-local.test.sh — CTX-1308 local Quality dispatch and cache regression tests. +set -euo pipefail + +cd "$(dirname "$0")/../.." +scratch="${TMPDIR:-/tmp}/bitty" +mkdir -p "$scratch" +TMP="$(mktemp -d "$scratch/ci-local-test.XXXXXX")" +trap 'rm -rf "$TMP"' EXIT +FAIL=0 + +fail() { + echo "FAIL: $*" >&2 + FAIL=1 +} + +# Run the real recipe with only its external container commands replaced. +# A separate Git repository also isolates the repository-wide act lock. +mkdir -p "$TMP/bin" "$TMP/repo" "$TMP/cargo" +git init --quiet "$TMP/repo" +cat >"$TMP/bin/docker" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +[[ "$*" == 'image inspect bitty-act:latest' ]] +STUB +cat >"$TMP/bin/act" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$@" >>"$CI_LOCAL_TEST_ARGS" +if [[ -n "${CI_LOCAL_TEST_FAIL_SHARD:-}" && " $* " == *" shard:$CI_LOCAL_TEST_FAIL_SHARD "* ]]; then + exit 23 +fi +exit "${CI_LOCAL_TEST_ACT_STATUS:-0}" +STUB +chmod +x "$TMP/bin/docker" "$TMP/bin/act" +export CI_LOCAL_TEST_ARGS="$TMP/args" + +# Extract the shebang recipe so CI does not need a second task runner just +# to test dispatch. Substitute the variadic argument exactly as just does. +awk ' + /^ci-local \*args:/ {recipe=1; next} + recipe && /^ / {sub(/^ /, ""); gsub(/\{\{args\}\}/, "--concurrent-jobs 1"); print; next} + recipe {exit} +' justfile >"$TMP/recipe.sh" +PATH="$TMP/bin:$PATH" CARGO_HOME="$TMP/cargo" BITTY_ACT_CACHE="$TMP/cache" \ + bash -c 'cd "$1" && bash "$2"' _ "$TMP/repo" "$TMP/recipe.sh" + +runner="$(awk '/^ quality:/{quality=1; next} quality && /^ runs-on:/{print $2; exit}' .github/workflows/ci.yml)" +grep -Fxq "$runner=bitty-act:latest" "$TMP/args" || fail 'Quality runner must map to the local image' +grep -Fq -- "-P $runner=bitty-act:latest" .github/act/Dockerfile || fail 'Dockerfile usage must match the Quality runner' +grep -Fxq 'quality' "$TMP/args" || fail 'dispatch must select the Quality job' +grep -Fxq -- '--concurrent-jobs' "$TMP/args" || fail 'caller arguments must reach act' +grep -Fxq -- '--bind' "$TMP/args" || fail 'local working tree must remain bind-mounted' +grep -Fq -- "$TMP/cache/target:/cache/target" "$TMP/args" || fail 'build/cache directory must persist' +grep -Fxq -- '--cache-server-addr' "$TMP/args" || fail 'local cache server must bind to proxy-exempt loopback' +grep -Fxq '127.0.0.1' "$TMP/args" || fail 'local cache endpoint must match NO_PROXY' +for shard in 1 2; do + grep -Fxq "shard:$shard" "$TMP/args" || fail "dispatch must explicitly execute shard $shard" +done +[[ "$(grep -Fxc 'quality' "$TMP/args")" == 2 ]] || fail 'both shards must run once in separate act invocations' + +# An early shard failure must propagate, without starting the next shard. +rm "$TMP/args" +status=0 +PATH="$TMP/bin:$PATH" CARGO_HOME="$TMP/cargo" BITTY_ACT_CACHE="$TMP/cache" \ + CI_LOCAL_TEST_ACT_STATUS=23 bash -c 'cd "$1" && bash "$2"' _ "$TMP/repo" "$TMP/recipe.sh" || status=$? +[[ "$status" == 23 ]] || fail 'a shard failure must fail dispatch' +[[ "$(grep -Fxc 'quality' "$TMP/args")" == 1 ]] || fail 'a failed shard must stop dispatch' + +# A passing first shard must not hide a failure in the second shard. +rm "$TMP/args" +status=0 +PATH="$TMP/bin:$PATH" CARGO_HOME="$TMP/cargo" BITTY_ACT_CACHE="$TMP/cache" \ + CI_LOCAL_TEST_FAIL_SHARD=2 bash -c 'cd "$1" && bash "$2"' _ "$TMP/repo" "$TMP/recipe.sh" || status=$? +[[ "$status" == 23 ]] || fail 'a second-shard failure must fail dispatch' +[[ "$(grep -Fxc 'quality' "$TMP/args")" == 2 ]] || fail 'second-shard failure must follow a passing first shard' + +# A competing invocation must fail before starting any container command. +( + flock -x 9 + rm "$TMP/args" + if PATH="$TMP/bin:$PATH" CARGO_HOME="$TMP/cargo" BITTY_ACT_CACHE="$TMP/cache" \ + bash -c 'cd "$1" && bash "$2"' _ "$TMP/repo" "$TMP/recipe.sh" >"$TMP/locked.log" 2>&1; then + fail 'a held repository lock must reject a second run' + fi + [[ ! -e "$TMP/args" ]] || fail 'a rejected run must not invoke act' + grep -Fq 'another ci-local run' "$TMP/locked.log" || fail 'lock contention must be explained' + exit "$FAIL" +) 9>"$TMP/repo/.git/ci-local.lock" || FAIL=1 + +# Execute the local-only workflow setup, verifying the environment seen by +# the unchanged sccache action. Separate shard directories avoid disk-cache +# server races when act runs both matrix entries concurrently. +setup="$(awk ' + /^ - name: Configure local sccache backend$/ {step=1; next} + step && /^ if:/ {next} + step && /^ run: \|/ {body=1; next} + body && /^ / {sub(/^ /, ""); print; next} + body {exit} +' .github/workflows/ci.yml)" +if [[ -z "$setup" ]]; then + fail 'local sccache setup step must exist' +else + awk '/^ - name: Configure local sccache backend$/ {getline; if ($0 != " if: env.ACT") exit 1; found=1} END {if (!found) exit 1}' \ + .github/workflows/ci.yml || fail 'local backend must be limited to act' + for shard in 1 2; do + : >"$TMP/env-$shard" + printf '%s\n' "$setup" | sed "s/\${{ matrix.shard }}/$shard/g" | + GITHUB_ENV="$TMP/env-$shard" CARGO_TARGET_DIR="$TMP/target" bash -e + grep -Fxq 'SCCACHE_GHA_ENABLED=false' "$TMP/env-$shard" || fail 'act must disable only the GHA backend' + grep -Fxq 'SCCACHE_GHA_VERSION=' "$TMP/env-$shard" || fail 'a GHA namespace must not re-enable the remote backend locally' + grep -Fxq "SCCACHE_DIR=$TMP/target/sccache-$shard" "$TMP/env-$shard" || fail 'local disk cache must persist per shard' + grep -Eq '^SCCACHE_CACHE_SIZE=[1-9][0-9]*[MG]$' "$TMP/env-$shard" || fail 'disk cache must have an explicit size bound' + done +fi +quality="$(awk '/^ quality:/{quality=1; next} quality && /^ [a-z]/{exit} quality {print}' .github/workflows/ci.yml)" +grep -Fq 'SCCACHE_GHA_ENABLED: "true"' <<<"$quality" || fail 'remote Quality GHA caching must remain enabled' +grep -Fq 'RUSTC_WRAPPER: "sccache"' <<<"$quality" || fail 'Quality compiler caching must remain enabled' + +if ((FAIL)); then + echo 'ci-local-test: FAIL' >&2 + exit 1 +fi +echo 'ci-local-test: OK'