Files
Chris Tate a22f2043d1 Make app.json the default manifest (#385)
* Make app.json the default manifest

- Add full-featured app.json parsing, discovery, build conversion, and app.zon fallback.
- Scaffold JSON manifests by default across TypeScript, Zig, web, full, and ejected projects.
- Publish the JSON Schema and update validation, vendoring, docs, skills, and npm payloads.

* Fix app.json manifest regressions

* Fix app.json conversion and schema bounds

* Publish versioned app schema

* Fix app.json vendoring and schema bounds
2026-08-18 07:41:43 -05:00

449 lines
18 KiB
Bash
Executable File

#!/usr/bin/env bash
# Tiered local gate for the Native SDK.
#
# scripts/gate.sh fast [base-ref] # affected-only: what your diff touches
# scripts/gate.sh full [base-ref] [--all] [--perf] # everything CI-shaped that runs locally
#
# fast — root `zig build test` + `zig build validate`, plus the suites for
# the examples AFFECTED by your diff against base-ref (default: main). The
# diff is `git diff --name-only` against `git merge-base base-ref HEAD`,
# plus untracked files, so uncommitted work counts. Framework diffs on
# macOS additionally run the render-benchmark budget ratchet (see below).
#
# Path -> step mapping (fast tier):
# src/**, build.zig, build.zig.zon, build/**,
# tools/**, tests/**, assets/** -> framework change: root suites
# + ALL example suites
# (frontends, native, mobile)
# src/platform/macos/** -> additionally cef-host-link:
# build the WebView example
# with -Dweb-engine=chromium so
# cef_host.mm actually compiles
# and links (macOS only; skipped
# with a loud warning when the
# CEF layout is absent — prime
# with `zig build &&
# ./zig-out/bin/native cef install`)
# examples/<name>/** -> that example's suite only
# (test-example-<name>; the
# mobile projects map to
# test-examples-mobile; hello/
# webview/browser run their
# in-dir `zig build test`)
# docs/**, apps/schema/** -> docs `pnpm check`
# docs/**, skills/**, skill-data/**,
# packages/core/** -> service-surface tooling tests:
# manifest diff categorization,
# generated compile-surface
# freshness, and hand-written
# SC-code/stale compiler-version
# claim checks (requires
# `npm ci --include=dev` in
# packages/core, like the
# service suites)
# **/src/services/**, service seam runtime,
# compiler, corewire, fixture, or skill paths -> real compiled service-host
# success/error/crash/timeout/
# replay battery + focused
# frontend/contract tests
# anything else (README, .github, packages,
# scripts, skills, release docs, ...) -> root suites only
# A docs-ONLY diff runs the docs and service-surface checks. The docs check
# is path-gated in both tiers: it never runs unless docs/ changed (or --all
# in full).
#
# full — root test + validate, the service-surface claim check, every
# example suite (frontends, native incl.
# canvas-preview, mobile), the Chromium host link check (cef-host-link),
# the macOS automation smokes (gpu-surface,
# gpu-dashboard, gpu-components, menu-bar, canvas-preview, writeback; skipped off-macOS), a
# markup check over every example markup file, and the docs check if docs/ changed
# vs base-ref or --all was passed. --perf additionally runs the percentile
# GPU perf check (test-gpu-dashboard-perf; macOS only, slow, load-sensitive —
# opt-in so a busy dev box doesn't fail the gate on noise).
#
# bench-check — the render-benchmark budget ratchet
# (`zig build bench-render -Doptimize=ReleaseFast -- --check
# tools/bench-render-budgets.txt`): median e2e p50 of three deterministic
# suite passes per scenario vs committed budgets with ~1.3x+ headroom, so
# it trips on order-of-magnitude regressions (a reintroduced O(n^2)
# planner scan, an extra emission per input), not machine noise. Unlike
# the --perf harness it is headless, in-process, and seconds-long once the
# ReleaseFast build is cached — that is why it runs by DEFAULT (fast tier:
# framework diffs only, since engine perf cannot regress from an
# example-only or docs diff; full tier: always) instead of opt-in like
# --perf: a ratchet that only runs when someone remembers to ask is not a
# ratchet. macOS-only because the budgets are calibrated on Apple Silicon.
# Set NATIVE_SDK_SKIP_BENCH_CHECK=1 to skip it on a box that is too busy
# even for the generous budgets.
#
# cross-e2e — the cross-target execution lane (scripts/cross-e2e.sh):
# cross-builds the TS-core/service batteries and app fixtures for
# x86_64-windows-gnu and x86_64-linux-musl, then executes the batteries
# on the Windows box (ssh windows-dev) and in a Linux container. Opt-in
# with NATIVE_SDK_CROSS=1 in either tier — it needs those machines —
# and skipped otherwise.
#
# mobile-e2e — the mobile execution lane (scripts/mobile-e2e.sh): builds
# the TS mobile battery and app fixtures for aarch64-ios-simulator and
# aarch64-linux-android, then executes the battery on a booted iPhone
# simulator (xcrun simctl) and a headless arm64 emulator (adb). Opt-in
# with NATIVE_SDK_MOBILE=1 in either tier — it needs Xcode's simulator
# runtime and the Android SDK/NDK — and skipped otherwise.
#
# Deliberately NOT `set -e`: every step runs even after a failure so the
# summary shows the whole picture; the exit code is non-zero if any step
# failed. Step output streams through; each step is timed.
set -u
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root" || exit 1
usage() {
echo "usage: scripts/gate.sh <fast|full> [base-ref] [--all] [--perf]" >&2
exit 2
}
tier="${1:-}"
case "$tier" in fast|full) ;; *) usage ;; esac
shift
base_ref="main"
run_all=false
run_perf=false
for arg in "$@"; do
case "$arg" in
--all) run_all=true ;;
--perf) run_perf=true ;;
-*) usage ;;
*) base_ref="$arg" ;;
esac
done
# ---- diff classification --------------------------------------------------
base_commit="$(git merge-base "$base_ref" HEAD 2>/dev/null)"
if [ -z "$base_commit" ]; then
echo "gate: cannot resolve merge-base of '$base_ref' and HEAD" >&2
exit 2
fi
changed_files="$( (git diff --name-only "$base_commit"; git ls-files --others --exclude-standard) | sort -u)"
framework_changed=false
macos_platform_changed=false
docs_changed=false
meta_changed=false
service_seam_changed=false
surface_claims_changed=false
affected_examples="" # space-separated example dir names
mobile_affected=false
# Examples with a root `test-example-<name>` step, derived from build.zig's
# `addExampleTestStep(...)` registrations so this script is never a second
# registry that drifts (it did, twice in one day — soundboard/calculator/
# notes were silently skipped). Layout/file-contains checks register other
# `test-example-*` names via different helpers and are deliberately not
# example suites, so the match is keyed on the helper name.
registered_examples="$(sed -n 's/.*addExampleTestStep([^"]*"test-example-\([A-Za-z0-9-]*\)".*/\1/p' build.zig | sort -u | tr '\n' ' ')"
if [ -z "$registered_examples" ]; then
echo "gate: derived no registered examples from build.zig (addExampleTestStep pattern matched nothing — helper renamed?)" >&2
exit 2
fi
# Examples whose suite is their own in-dir `zig build test`.
indir_examples="browser hello webview"
# Mobile example projects, covered as a group by test-examples-mobile.
mobile_examples="android ios mobile-canvas mobile-shell"
note_example() {
case " $affected_examples " in *" $1 "*) ;; *) affected_examples="$affected_examples $1" ;; esac
}
while IFS= read -r file; do
[ -n "$file" ] || continue
case "$file" in
docs/*) docs_changed=true ;;
apps/schema/*) docs_changed=true; meta_changed=true ;;
packages/core/*) meta_changed=true ;;
src/platform/macos/*) framework_changed=true; macos_platform_changed=true ;;
src/*|build.zig|build.zig.zon|build/*|tools/*|tests/*|assets/*) framework_changed=true ;;
examples/*/*)
example="${file#examples/}"
example="${example%%/*}"
case " $mobile_examples " in
*" $example "*) mobile_affected=true ;;
*) note_example "$example" ;;
esac
;;
*) meta_changed=true ;;
esac
case "$file" in
*/src/services/*|src/runtime/service_host.zig|src/runtime/service_pool.zig|src/app_runner/ts_core_main.zig|tests/ts-services/*|tools/corewire/emit_service.zig|tools/corewire/service_contract.zig|packages/core/src/checker.ts|packages/core/src/cli.ts|packages/core/src/diagnostics.ts|packages/core/src/frontend.ts|packages/core/src/modules.ts|packages/core/src/service_contract.ts|packages/core/src/typed_ast.ts|packages/core/scripts/compiler_typecheck.mjs|packages/core/scripts/stage_external_services.mjs|packages/core/scripts/run_external_service_compiler.mjs|skill-data/ts-services/*|packages/native-sdk/*service_host*|build.zig|build/app.zig)
service_seam_changed=true
;;
esac
case "$file" in
docs/*|skills/*|skill-data/*|packages/core/*) surface_claims_changed=true ;;
esac
done <<EOF_FILES
$changed_files
EOF_FILES
# ---- step machinery -------------------------------------------------------
step_names=""
step_status=""
step_secs=""
failures=0
gate_start=$(date +%s)
record() { # name status seconds
step_names="$step_names$1|"
step_status="$step_status$2|"
step_secs="$step_secs$3|"
}
run_step() { # name command...
name="$1"; shift
echo ""
echo "==> $name: $*"
start=$(date +%s)
"$@"
rc=$?
secs=$(( $(date +%s) - start ))
if [ "$rc" -eq 0 ]; then
record "$name" PASS "$secs"
else
record "$name" FAIL "$secs"
failures=$((failures + 1))
echo "==> $name FAILED (exit $rc)" >&2
fi
}
skip_step() { # name reason
echo ""
echo "==> $1: skipped ($2)"
record "$1" SKIP 0
}
docs_check() {
run_step "docs-install" pnpm --dir docs install --frozen-lockfile
# Build into a gate-owned dist dir so the check never corrupts a running
# dev server's .next cache.
run_step "docs-check" env NEXT_DIST_DIR=.next-gate pnpm --dir docs check
}
is_macos=false
[ "$(uname -s)" = "Darwin" ] && is_macos=true
bench_check() {
zig build bench-render -Doptimize=ReleaseFast -- --check "$repo_root/tools/bench-render-budgets.txt"
}
run_bench_check_step() { # runs (or explains skipping) the render-benchmark ratchet
if ! $is_macos; then
skip_step "bench-check" "budgets calibrated on Apple Silicon macOS"
elif [ -n "${NATIVE_SDK_SKIP_BENCH_CHECK:-}" ]; then
skip_step "bench-check" "NATIVE_SDK_SKIP_BENCH_CHECK set"
else
run_step "bench-check" bench_check
fi
}
run_cross_e2e_step() { # the opt-in cross-target execution lane
if [ "${NATIVE_SDK_CROSS:-}" = "1" ]; then
run_step "cross-e2e" scripts/cross-e2e.sh
else
skip_step "cross-e2e" "opt-in: set NATIVE_SDK_CROSS=1 (needs the windows-dev box and Docker)"
fi
}
run_mobile_e2e_step() { # the opt-in mobile execution lane
if [ "${NATIVE_SDK_MOBILE:-}" = "1" ]; then
run_step "mobile-e2e" scripts/mobile-e2e.sh
else
skip_step "mobile-e2e" "opt-in: set NATIVE_SDK_MOBILE=1 (needs Xcode's simulator runtime and the Android SDK/NDK)"
fi
}
# The Chromium (CEF) host, src/platform/macos/cef_host.mm, is only compiled
# by Chromium-engine app builds — never by `zig build test`/`validate` — so
# without this step an edit that keeps it merely grep-identical to the
# AppKit host can land without ever seeing a compiler. test-webview-cef-link
# builds (and links) the WebView example against the host for real.
cef_layout_ready() {
[ -f third_party/cef/macos/include/cef_app.h ] &&
[ -d "third_party/cef/macos/Release/Chromium Embedded Framework.framework" ] &&
[ -f third_party/cef/macos/libcef_dll_wrapper/libcef_dll_wrapper.a ]
}
cef_host_step() {
if ! $is_macos; then
skip_step "cef-host-link" "macOS only"
return
fi
if ! cef_layout_ready; then
echo "" >&2
echo "==> cef-host-link: WARNING — src/platform/macos changed but the CEF layout is absent," >&2
echo " so the Chromium host was NOT compiled. Blind cef_host.mm edits will not be caught." >&2
echo " Prime it once (downloads the prepared CEF runtime into third_party/cef/macos):" >&2
echo " zig build && ./zig-out/bin/native cef install" >&2
skip_step "cef-host-link" "CEF layout absent at third_party/cef/macos (see warning)"
return
fi
run_step "cef-host-link" zig build test-webview-cef-link
}
# ---- tiers ----------------------------------------------------------------
if [ "$tier" = "fast" ]; then
non_docs_change=false
{ $framework_changed || $meta_changed || $mobile_affected || [ -n "$affected_examples" ]; } && non_docs_change=true
if $non_docs_change || ! $docs_changed; then
# Root suites run for any non-docs diff, and also for an empty diff
# (a clean tree still deserves a baseline check).
run_step "zig-test" zig build test
run_step "zig-validate" zig build validate
else
skip_step "zig-test" "docs-only diff"
skip_step "zig-validate" "docs-only diff"
fi
if $framework_changed; then
run_step "examples-frontends" zig build test-examples-frontends
run_step "examples-native" zig build test-examples-native
run_step "examples-mobile" zig build test-examples-mobile
# Engine perf can only regress through framework code, so the ratchet
# rides the same trigger as the full example sweep.
run_bench_check_step
else
for example in $affected_examples; do
case " $registered_examples " in
*" $example "*) run_step "example-$example" zig build "test-example-$example" ;;
*)
case " $indir_examples " in
*" $example "*) run_step "example-$example" sh -c "cd 'examples/$example' && zig build test -Dplatform=null" ;;
*) skip_step "example-$example" "no test suite registered for examples/$example" ;;
esac
;;
esac
done
$mobile_affected && run_step "examples-mobile" zig build test-examples-mobile
fi
if $service_seam_changed; then
run_step "ts-services-frontend" node --test packages/core/test/services.test.ts
run_step "ts-services-e2e" zig build test-ts-services-e2e
else
skip_step "ts-services-frontend" "service seam paths unchanged vs $base_ref"
skip_step "ts-services-e2e" "service seam paths unchanged vs $base_ref"
fi
if $surface_claims_changed; then
run_step "surface-claims" node --test packages/core/test/surface_tools.test.ts
else
skip_step "surface-claims" "docs/skills/skill-data/packages-core unchanged vs $base_ref"
fi
if $macos_platform_changed; then
cef_host_step
else
skip_step "cef-host-link" "src/platform/macos unchanged vs $base_ref"
fi
if $docs_changed; then
docs_check
else
skip_step "docs-check" "docs/ unchanged vs $base_ref"
fi
run_cross_e2e_step
run_mobile_e2e_step
else # full
run_step "zig-test" zig build test
run_step "zig-validate" zig build validate
run_step "ts-services-frontend" node --test packages/core/test/services.test.ts
run_step "ts-services-e2e" zig build test-ts-services-e2e
run_step "surface-claims" node --test packages/core/test/surface_tools.test.ts
run_step "examples-frontends" zig build test-examples-frontends
run_step "examples-native" zig build test-examples-native
run_step "examples-mobile" zig build test-examples-mobile
cef_host_step
if $is_macos; then
run_step "smoke-gpu-surface" zig build test-gpu-surface-smoke
run_step "smoke-gpu-dashboard" zig build test-gpu-dashboard-smoke
run_step "smoke-gpu-components" zig build test-gpu-components-smoke
run_step "smoke-webview" zig build test-webview-smoke
run_step "smoke-native-shell" zig build test-native-shell-smoke
run_step "smoke-menu-bar" zig build test-menu-bar-smoke
run_step "smoke-canvas-preview" zig build test-canvas-preview-smoke
run_step "smoke-writeback" zig build test-writeback-smoke
else
skip_step "smoke-gpu-surface" "macOS only"
skip_step "smoke-gpu-dashboard" "macOS only"
skip_step "smoke-gpu-components" "macOS only"
skip_step "smoke-webview" "macOS only"
skip_step "smoke-native-shell" "macOS only"
skip_step "smoke-menu-bar" "macOS only"
skip_step "smoke-canvas-preview" "macOS only"
skip_step "smoke-writeback" "macOS only"
fi
run_bench_check_step
if $run_perf; then
if $is_macos; then
run_step "perf-gpu-dashboard" zig build test-gpu-dashboard-perf
else
skip_step "perf-gpu-dashboard" "macOS only"
fi
else
skip_step "perf-gpu-dashboard" "opt-in: pass --perf (slow, load-sensitive)"
fi
markup_check() {
zig build || return 1
# shellcheck disable=SC2046
# -type f: zero-config app builds leave a .native/ cache DIRECTORY in
# example dirs, which the name pattern would otherwise match.
./zig-out/bin/native markup check $(find examples -name '*.native' -type f | sort)
}
run_step "markup-check" markup_check
if $docs_changed || $run_all; then
docs_check
else
skip_step "docs-check" "docs/ unchanged vs $base_ref (pass --all to force)"
fi
run_cross_e2e_step
run_mobile_e2e_step
fi
# ---- summary --------------------------------------------------------------
total_secs=$(( $(date +%s) - gate_start ))
echo ""
echo "==================== gate $tier summary (base: $base_ref) ===================="
old_ifs="$IFS"; IFS='|'
set -- $step_names
names=("$@")
set -- $step_status
statuses=("$@")
set -- $step_secs
secs=("$@")
IFS="$old_ifs"
i=0
while [ "$i" -lt "${#names[@]}" ]; do
printf ' %-22s %-4s %4ss\n' "${names[$i]}" "${statuses[$i]}" "${secs[$i]}"
i=$((i + 1))
done
printf ' %-22s %-4s %4ss\n' "total" "$([ "$failures" -eq 0 ] && echo PASS || echo FAIL)" "$total_secs"
if [ "$failures" -gt 0 ]; then
echo "gate $tier: $failures step(s) failed" >&2
exit 1
fi
echo "gate $tier: all steps green"