Skip to content

fix(fork): prevent false Codex input alerts #208

fix(fork): prevent false Codex input alerts

fix(fork): prevent false Codex input alerts #208

Workflow file for this run

name: run_tests
on:
push:
branches: [main]
# No base-branch filter on pull_request: #184 lands as a stack of PRs
# (each phase based on the previous one), and a stacked PR must get the
# same checks as one against main, or its "Done when" is only ever
# verified after the base merges.
pull_request:
# Match Zed's pattern (run_tests.yml): on PR branches, collapse runs on the
# same ref so a new push cancels any in-flight CI for that branch. On main,
# every commit gets its own group (keyed by SHA) so we never cancel a real
# post-merge run. Without this, fast successive pushes on a feature branch
# pile up 30-min-cancelled runs in the history.
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true
# Least-privilege default for GITHUB_TOKEN. Every job reads repository contents
# (checkout + cargo + cache); the path-filter job alone elevates to read the
# PR's changed-file list. If a future job ever needs write (push a tag, post a
# PR comment), it must elevate explicitly at the job level, making the
# escalation visible in code review.
permissions:
contents: read
# Match Zed's run_tests.yml: -e fail-on-error, -u error-on-unset, -x
# trace each command (CI logs stay greppable), -o pipefail propagate
# pipe failures.
defaults:
run:
shell: bash -euxo pipefail {0}
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
jobs:
# Mirrors Zed's `orchestrate` job pattern (run_tests.yml in
# zed-industries/zed): a cheap path filter that gates every
# expensive downstream leg on what the PR actually touched.
#
# Outputs:
# - rust: any Rust source or manifest changed
# - rendering: the GPUI paint / font / theme paths the visual
# smoke job is designed to protect
# - changed_packages: space-separated cargo workspace member names
# whose source tree changed. Empty string means
# "workspace-wide change OR cannot compute" -
# downstream consumers must treat empty as
# "everything changed" (fail-safe). Computed
# dynamically via `cargo metadata --no-deps`
# so adding new workspace members requires no
# workflow update.
#
# Gating posture: macos-15 is the only compile runner and is the
# bulk of CI wall-clock. The path filter short-circuits it on
# docs-only / non-workflow .github/** PRs.
#
# Filter scope: CI, release, security, and packaging files get their
# own signals so workflow-gate changes cannot self-skip into a false
# green. Rust jobs still skip for docs-only changes.
orchestrate:
name: Detect changed paths
runs-on: macos-15
permissions:
contents: read
pull-requests: read
outputs:
rust: ${{ steps.filter.outputs.rust }}
rendering: ${{ steps.filter.outputs.rendering }}
ci: ${{ steps.filter.outputs.ci }}
security: ${{ steps.filter.outputs.security }}
release_packaging: ${{ steps.filter.outputs.release_packaging }}
changed_packages: ${{ steps.detect_packages.outputs.changed_packages }}
steps:
# fetch-depth tuned for the `detect_packages` git diff below: on a
# PR we need enough history to reach the merge-base with the base
# branch (~350 commits matches Zed's pattern, comfortably covers
# long-running feature branches); on a push to main HEAD~1 is
# enough.
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 350 || 2 }}
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
id: filter
with:
filters: |
rust:
- 'src-app/**/*.rs'
- 'crates/**/*.rs'
- '**/Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'clippy.toml'
# On-disk drift tests / include_str! targets, not "docs-only".
# cargo test reads these files; a schema- or example-only PR
# must run macos_check so drift cannot land green.
# public_json_schema_covers_every_config_field reads schemas/
# public_configuration_schema_doc_mentions_schema_keys reads
# docs/user/configuration/schema.md
# demo_review_pipeline_flow_is_valid include_str!'s
# examples/review-pipeline.flow.toml (TASK.md is its fixture)
- 'schemas/**'
- 'docs/user/configuration/schema.md'
- 'examples/**'
# The -sys build script and its tests read native/libghostty
# (manifest, bindings, header, archive, build-info) and the
# tests pin their hashes; an archive- or manifest-only PR is
# not docs-only.
- 'native/**'
ci:
- '.github/workflows/**'
- 'scripts/**'
security:
- '**/Cargo.toml'
- 'Cargo.lock'
- 'deny.toml'
- '.github/workflows/audit.yml'
- '.github/workflows/run_tests.yml'
release_packaging:
- '.github/workflows/release.yml'
- 'packaging/**'
- 'assets/**'
- 'scripts/**'
- 'docs/release-runbook.md'
- 'docs/release/**'
rendering:
# GPUI Element + paint pipeline (where c3e2331 lived)
- 'src-app/src/terminal/element/**'
- 'src-app/src/terminal/view.rs'
# Font resolution + embedded fallback wiring
- 'src-app/src/fonts.rs'
- 'src-app/src/terminal/element/font.rs'
# Theme model + active_theme() (affects glyph colors,
# contrast, and the resolved family chain on macOS Core
# Text).
- 'src-app/src/theme/**'
# Window chrome / CSD - affects content rect + DPR on
# macOS (NSWindow).
- 'src-app/src/window_chrome/**'
# Pane / layout - emits the GPUI flex tree that hosts the
# terminal element; a regression here can produce 0-sized
# frames that no-op the paint pass without crashing.
- 'src-app/src/pane.rs'
- 'src-app/src/layout/render.rs'
- 'src-app/tests/flex_nchild.rs'
# Per-package detector. Adapted from Zed's orchestrate step. The
# output isn't consumed yet (`cargo test --workspace` still runs
# cheaply enough), but exposing it now lets
# future jobs gate per-crate (e.g. when crate count grows or
# nextest is adopted). Empty `changed_packages` means "treat as
# full rebuild" - fail-safe default.
#
# cargo + jq are preinstalled on macos-15. cargo metadata's
# output is JSON; jq extracts the member-name × directory map.
- id: detect_packages
name: Detect changed Cargo packages
shell: bash
run: |
set -euo pipefail
if [ -z "${GITHUB_BASE_REF:-}" ]; then
echo "Push event - comparing against HEAD~1"
COMPARE_REV="$(git rev-parse HEAD~1 2>/dev/null || true)"
else
echo "PR event - comparing against merge-base with ${GITHUB_BASE_REF}"
git fetch origin "$GITHUB_BASE_REF" --depth=350 || true
COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD 2>/dev/null || true)"
fi
if [ -z "$COMPARE_REV" ]; then
echo "Cannot determine compare base - treating as full rebuild"
echo "changed_packages=" >> "$GITHUB_OUTPUT"
exit 0
fi
CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" "$GITHUB_SHA" || true)"
echo "$CHANGED_FILES" | head -20
# Workspace-wide change → bypass per-package detection. Matches
# Zed's pattern: any edit to the toolchain pin, cargo config,
# root manifest/lockfile, or workflow files forces a full
# rebuild because the change can affect every crate.
if echo "$CHANGED_FILES" | grep -qE '^(rust-toolchain\.toml|\.cargo/|Cargo\.lock$|Cargo\.toml$|\.github/workflows/)'; then
echo "Workspace-wide change detected - emitting empty changed_packages (treat as full rebuild)"
echo "changed_packages=" >> "$GITHUB_OUTPUT"
exit 0
fi
# Build dir→pkg map dynamically from cargo metadata so new
# workspace members are auto-detected. manifest_path looks
# like `/.../src-app/Cargo.toml` or
# `/.../crates/paneflow-config/Cargo.toml`; capture the leaf
# directory.
DIR_TO_PKG="$(cargo metadata --format-version=1 --no-deps 2>/dev/null \
| jq -r '
.packages[]
| select(.manifest_path | test("/(src-app|crates/[^/]+)/Cargo\\.toml$"))
| (.manifest_path | capture("/(?<dir>src-app|crates/[^/]+)/Cargo\\.toml$") | .dir)
+ "=" + .name
' || true)"
echo "Workspace member map:"
echo "$DIR_TO_PKG"
if [ -z "$DIR_TO_PKG" ]; then
echo "cargo metadata returned no members - treating as full rebuild"
echo "changed_packages=" >> "$GITHUB_OUTPUT"
exit 0
fi
# For each member dir, check whether any file under it changed.
CHANGED_PKGS=""
while IFS='=' read -r dir pkg; do
[ -n "$dir" ] || continue
if echo "$CHANGED_FILES" | grep -qE "^${dir}/"; then
CHANGED_PKGS="${CHANGED_PKGS}${pkg} "
fi
done <<< "$DIR_TO_PKG"
# Empty CHANGED_PKGS means the diff touched files outside every
# workspace member dir (e.g. root-level deny.toml, docs/, tasks/).
# Treat as a no-op for per-package gating - downstream consumers
# interpret empty changed_packages as "full rebuild" (fail-safe).
# The early-return avoids piping empty input into `grep -v '^$'`,
# which exits 1 on no-match and trips `set -euo pipefail` because
# bash propagates command-substitution exit status under `set -e`.
if [ -z "${CHANGED_PKGS// /}" ]; then
echo "No workspace member touched - emitting empty changed_packages"
echo "changed_packages=" >> "$GITHUB_OUTPUT"
exit 0
fi
CHANGED_PKGS="$(echo "$CHANGED_PKGS" | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ' | sed 's/ $//')"
echo "Changed packages: '${CHANGED_PKGS}'"
echo "changed_packages=${CHANGED_PKGS}" >> "$GITHUB_OUTPUT"
# Platform-residue census (issue #69). `scripts/linux-census.sh` and
# `scripts/win-census.sh` grep the tree for the Linux / Windows cfg
# predicates, Cargo target tables, and target-triple string checks that
# stages 2b/2c removed. Each script exits 1 when its zero-condition is
# non-zero OR when its negative control (live `cfg(unix)` /
# `cfg(target_os = "macos")` counts) collapses to 0, so a broken regex
# reads as red, not as a clean tree.
#
# bash + grep + python3 only; nothing compiles, so this is an Ubuntu lane
# (issue #2: non-compile jobs do not belong on macos-15). It is not gated
# on `orchestrate`: it costs seconds, and the census scans every `*.rs`
# and `Cargo.toml` in the tree, which is wider than the `rust` path
# filter, so an unconditional run cannot self-skip into a false green.
platform_census:
name: Platform census (linux / windows residue)
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: scripts/linux-census.sh
run: ./scripts/linux-census.sh
- name: scripts/win-census.sh
run: ./scripts/win-census.sh
# cargo-deny gate. BLOCKING since 2026-05-27 - promoted from
# warn-only after the lockfile reached audit-clean (Marvin Attack
# ignored with documented rationale, GPL-3 transitive deps from
# GPUI accepted globally, bitstream-io 4.10 dropped yanked core2).
#
# The graduated severity belongs to deny.toml, not to a CI flag:
# * advisories.vulnerabilities → hard error (deny, cargo-deny default)
# * advisories.unmaintained → workspace-scoped warn
# * advisories.yanked → warn
# * advisories.ignore = [...] → explicit list with re-evaluation
# triggers (per-advisory rationale documented inline)
# * licenses → allow-list including GPL-3 + GPL-3-or-later
# * sources → unknown-registry / unknown-git = deny
# `unused-ignored-advisory = "warn"` in deny.toml surfaces stale
# ignores as the dep graph evolves, so the list doesn't rot.
#
# The previous `continue-on-error: true` posture was an anti-pattern
# (shnatsel's cargo-audit retirement post explicitly names it):
# the job appeared green in the UI, branch protection became
# decorative, and a real CVE could ship while looking the same as
# a noisy false positive. Following Bevy's reference pattern
# (bevyengine/bevy/.github/workflows/dependencies.yml).
#
# Gate on dependency/security inputs plus CI changes, so deny.toml and
# audit workflow edits cannot land with the audit job skipped.
# cargo-deny does not compile GPUI; it only needs the lockfile.
security_audit:
name: Security Audit (cargo-deny)
needs: orchestrate
if: >-
needs.orchestrate.outputs.rust == 'true' ||
needs.orchestrate.outputs.security == 'true' ||
needs.orchestrate.outputs.ci == 'true'
runs-on: macos-15
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 (master as of 2026-08-05)
with:
toolchain: "1.98.0"
# Same cache prefix-key as macos_check so rust-cache slots stay
# in one family. cargo-deny only needs the lockfile resolve.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
prefix-key: "v1-rust"
key: security-audit
- name: Install cargo-deny (pinned 0.19.x)
# `--locked` pins cargo-deny's own dep graph for reproducibility.
# `--version '^0.19'` accepts any 0.19.x patch but not 0.20+
# (verified against deny.toml semantics: 0.16+ introduces the
# `unmaintained = "workspace"` scope and `[[licenses.exceptions]]`
# syntax we rely on; 0.19 is the current stable upstream and
# the version validated locally before bumping).
run: cargo install cargo-deny --locked --version '^0.19'
- name: cargo deny check advisories licenses sources
# Retry once with 30s backoff on a DB-fetch failure (GitHub's
# RustSec mirror occasionally rate-limits). A second failure
# surfaces `cargo deny check advisories --help` for debugging
# alongside the RustSec advisory site URL in the job output.
# AC7: advisory DB unreachable must not silently pass.
run: |
set -euo pipefail
if ! cargo deny check advisories licenses sources; then
echo "→ cargo-deny failed on first attempt - sleeping 30s and retrying once (probable RustSec DB rate-limit)" >&2
sleep 30
if ! cargo deny check advisories licenses sources; then
echo "::error::cargo-deny check failed after retry. See https://embarkstudios.github.io/cargo-deny/ and the --help output below."
cargo deny check advisories --help || true
exit 1
fi
fi
# Apple Silicon quality gate: fmt, clippy, test, check, and a
# release build of paneflow-app. Style checks that used to live on a
# separate runner run here so this is the only compile leg.
#
# Runner: macos-15 (Apple Silicon). GitHub-hosted macos-15 defaults
# to Xcode 16.4; DEVELOPER_DIR pins that so an image refresh cannot
# silently swap the compiler to a newer Xcode line.
#
# Fallback: if a PR consistently exceeds 20 min wall time on macos-15,
# swap to `macos-15-xlarge` (paid tier) and note the rationale in the
# PR description.
macos_check:
name: macOS aarch64 smoke build
# Only run when Rust-relevant files change. `needs: orchestrate`
# waits for the cheap filter job and short-circuits the macos-15
# runner on docs-only / non-workflow .github/** PRs.
needs: orchestrate
if: >-
needs.orchestrate.outputs.rust == 'true' ||
needs.orchestrate.outputs.ci == 'true' ||
needs.orchestrate.outputs.release_packaging == 'true'
runs-on: macos-15
env:
DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
# Fail fast if the runner image drifts away from the pinned Xcode
# or the Metal compiler. GPUI compiles .metal shaders at build
# time; `xcrun metal --version` is the proof (path lookup succeeds
# even when the toolchain component is absent).
- name: Verify Xcode + Metal toolchain
run: |
set -euo pipefail
if [ ! -d "${DEVELOPER_DIR}" ]; then
echo "::error::Pinned Xcode not found at ${DEVELOPER_DIR}. macos-15 should ship Xcode 16.4; this indicates a runner image drift."
exit 1
fi
sudo xcode-select -s "${DEVELOPER_DIR}"
CLT_PATH="$(xcode-select -p 2>/dev/null || true)"
if [ -z "$CLT_PATH" ] || [ ! -d "$CLT_PATH" ]; then
echo "::error::Xcode developer directory not found (xcode-select -p returned '$CLT_PATH'). GitHub macos-15 runners should ship Xcode preinstalled - this indicates a runner image drift or a misconfigured self-hosted runner."
exit 1
fi
echo "Xcode developer dir: $CLT_PATH"
SDK_PATH="$(xcrun --show-sdk-path 2>/dev/null || true)"
if [ -z "$SDK_PATH" ] || [ ! -d "$SDK_PATH" ]; then
echo "::error::macOS SDK not found (xcrun --show-sdk-path returned '$SDK_PATH')."
exit 1
fi
echo "macOS SDK path: $SDK_PATH"
if ! xcrun metal --version; then
echo "::warning::xcrun metal --version failed; downloading MetalToolchain"
xcodebuild -downloadComponent MetalToolchain
if ! xcrun metal --version; then
echo "::error::Metal compiler still missing after MetalToolchain download. gpui compiles .metal shaders at build time and cannot proceed."
exit 1
fi
fi
METAL_HEADERS="$SDK_PATH/System/Library/Frameworks/Metal.framework/Headers"
if [ ! -d "$METAL_HEADERS" ]; then
echo "::error::Metal framework headers not found at $METAL_HEADERS. gpui Objective-C bridging will fail with header-not-found errors."
exit 1
fi
echo "Metal framework headers: $METAL_HEADERS"
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 (master as of 2026-08-05)
with:
toolchain: "1.98.0"
targets: aarch64-apple-darwin
# cargo fmt + cargo clippy need rustfmt + clippy, which
# dtolnay's action does NOT install by default even when
# `targets:` is set.
components: rustfmt, clippy
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
prefix-key: "v1-rust"
key: aarch64-apple-darwin
- name: cargo fmt --check
run: cargo fmt --check
# clippy is scoped to the aarch64-apple-darwin target so it
# type-checks platform-gated modules (runtime_paths.rs macOS arm,
# install_method::AppBundle, update_checker::AssetFormat::Dmg,
# etc.).
# `--all-targets` is REQUIRED: without it clippy never sees test-only
# modules, so a lint in a `#[cfg(test)]` block passes CI and fails the
# local gate that CLAUDE.md prescribes. Neither form is a superset of
# the other, so the two must not drift.
- name: cargo clippy (aarch64-apple-darwin)
run: cargo clippy --workspace --all-targets --locked --target aarch64-apple-darwin -- -D warnings
# cargo test on the workspace. `--target` pins compilation to
# aarch64-apple-darwin so test binaries are native Mach-O and
# the test run exercises the real macOS codepath.
- name: cargo test --workspace
run: cargo test --workspace --locked --target aarch64-apple-darwin
# The libghostty PTY smoke is a binary, not a #[test]: it opens a real
# PTY, drives /bin/sh through the linked archive, and checks the
# snapshot, the resize probe and the reap. Run it so the vendored
# archive is exercised end to end, not only linked.
- name: libghostty PTY smoke
run: cargo run --locked --target aarch64-apple-darwin -p paneflow-ghostty-smoke
# cargo check must exit 0 with no linker errors.
- name: cargo check (aarch64-apple-darwin)
run: cargo check --workspace --locked --target aarch64-apple-darwin
# release build produces the expected binary path.
# src-app/build.rs enforces EMBED_SIZE_LIMIT_BYTES on this build;
# there is no separate YAML size-budget step.
- name: cargo build --release (aarch64-apple-darwin)
run: cargo build --release -p paneflow-app --locked --target aarch64-apple-darwin
# verify the produced binary is a Mach-O arm64 executable.
- name: Verify Mach-O arm64 binary
run: |
BIN=target/aarch64-apple-darwin/release/paneflow
test -f "$BIN"
file "$BIN"
file "$BIN" | grep -q 'Mach-O 64-bit executable arm64'
# on failure the job logs already capture the failing crate and
# error; uploading the binary on success gives reviewers an artifact
# they can `file`/`otool` locally if something downstream misbehaves.
- name: Upload aarch64 binary artifact
if: success()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: paneflow-aarch64-apple-darwin
path: target/aarch64-apple-darwin/release/paneflow
if-no-files-found: error
retention-days: 7
# ───────────────────────────────────────────────────────────────────────
# Visual rendering smoke test - macOS aarch64.
#
# Catches the exact class of regression fixed in commit c3e2331
# ("fix(font): macOS empty-text rendering across sidebar, tabs, and
# terminal"): a PaneFlow build that compiles, links, and notarizes
# cleanly but renders zero glyphs because Core Text inside a `.app`
# bundle silently fails to surface the configured monospace family
# and no embedded font fallback is wired. The release pipeline never
# noticed because no leg ever launched the `.app` - every gate was
# compile/sign/notarize, none was visual. This job closes that hole at
# the PR gate so the regression cannot reach `main`.
#
# Pipeline: download the binary built by `macos_check` → wrap it in
# the same `.app` skeleton `release.yml` ships → launch the binary
# inside the bundle (so Core Text sees the bundle context, exactly
# like an end-user install) → screencapture → soft OCR sanity check.
# The screenshot is always uploaded so reviewers can eyeball the
# PR's macOS rendering even when the OCR gate is happy.
#
# Gates:
# * HARD - `font: resolved family=...` line MUST appear in the app
# log (proves the renderer reached `cached_font_config()`, i.e.
# the app didn't crash before first frame). This is the font-kit
# empty-box check.
# * SOFT - OCR finds ≥10 alphanumeric chars (the macOS menubar
# alone passes this trivially when rendering works; failure
# means we're staring at a black screen or empty cells).
#
# Runner: macos-15 hosts a real macOS GUI session, so apps launch
# and render normally — the same pattern used by Electron /
# Flutter / native-Cocoa CI setups.
# ───────────────────────────────────────────────────────────────────────
macos_render_smoke:
name: macOS aarch64 render smoke (visual)
needs: [orchestrate, macos_check]
# Gate on the `rendering` sub-filter (not the broad `rust` filter):
# the c3e2331-class regression this job exists to catch can only
# ride in on edits to the rendering paths declared by the
# orchestrate job. A pure config-file or non-render Rust PR cannot
# regress macOS Core Text empty-text rendering, so spending
# 10-15 min on a macos-15 runner on every such PR is wasted spend.
if: >-
needs.orchestrate.outputs.rendering == 'true' ||
needs.orchestrate.outputs.ci == 'true' ||
needs.orchestrate.outputs.release_packaging == 'true'
runs-on: macos-15
timeout-minutes: 15
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Download paneflow binary
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: paneflow-aarch64-apple-darwin
path: target/aarch64-apple-darwin/release/
- name: Restore executable bit
# actions/upload-artifact strips the executable bit on macOS
# outputs (https://github.com/actions/upload-artifact/issues/38).
# Without this, `bundle-macos.sh`'s `install -m 0755` would copy
# a non-executable file and `open` would fail with EACCES.
run: chmod +x target/aarch64-apple-darwin/release/paneflow
- name: Bundle .app
# Reuse the production bundling script so the smoke test exercises
# the SAME `.app` skeleton end-users get - Info.plist, .icns,
# MacOS/paneflow layout. Unsigned: signing is not what the bug
# was about (the original c3e2331 root cause reproduced equally
# in unsigned bundles per Core Text's bundle-context behavior),
# and skipping codesign keeps this gate free of Apple secrets.
run: |
set -euo pipefail
bash scripts/bundle-macos.sh --version "0.0.0-ci" --arch aarch64
- name: Launch .app and capture screenshot
run: |
set -euo pipefail
APP="dist/PaneFlow.app"
# Launch the binary INSIDE the bundle directly (rather than
# `open -a`) so stdout/stderr can be redirected - `open` does
# not tee child output and the `font: resolved family=` log
# line is our hard gate. The bundle path is preserved, so
# Info.plist + Resources/ load correctly and Core Text sees
# the same bundle context as a real install.
PANEFLOW_DISABLE_SPARKLE=1 RUST_LOG=info "$APP/Contents/MacOS/paneflow" \
>paneflow.log 2>&1 &
APP_PID=$!
# Cold-launch on macos-15: GPUI window creation + Metal init
# + first paint typically lands under 5s. 10s gives generous
# slack for runner contention.
sleep 10
# `-x` skips the camera-shutter sound; captures every display.
screencapture -x screenshot.png
# Clean shutdown so the runner doesn't carry a live paneflow
# process into the next step. SIGTERM lets GPUI's Drop chain
# (PTY shutdown, IPC socket cleanup) run; SIGKILL is the belt
# in case the main thread is blocked.
kill "$APP_PID" 2>/dev/null || true
sleep 2
kill -9 "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
# HARD GATE: the resolved-font log line must be present.
# Absence means the app crashed before reaching
# cached_font_config() - i.e. before the renderer ever asked
# for a font, which is upstream of every rendering-correctness
# property this job exists to verify.
echo "── Resolved font ──"
if grep "font: resolved family=" paneflow.log; then
if grep -q "font: resolved family='JetBrains Mono'" paneflow.log; then
echo "::warning::Embedded JetBrains Mono fallback was selected - Core Text did not surface a system mono family (Menlo / Monaco / Courier). The fallback is the safety net working as designed, but persistent selection on macos-15 indicates a Core Text drift worth investigating."
fi
else
echo "::error::No 'font: resolved family=' line in app log. The app likely crashed before first render - paneflow.log + screenshot.png artifacts will pin down the exact failure."
exit 1
fi
- name: OCR sanity check (text rendered?)
# Soft gate: tesseract output is fuzzy and we don't want flaky
# OCR to block PRs. The signal still surfaces in the CI log
# (alphanumeric char count) and the screenshot artifact is
# available for human review on every run.
continue-on-error: true
run: |
set -euo pipefail
brew install --quiet tesseract
text="$(tesseract screenshot.png - 2>/dev/null || true)"
echo "── OCR output (first 50 lines) ──"
echo "$text" | head -50
char_count="$(echo "$text" | tr -cd '[:alnum:]' | wc -c | tr -d ' ')"
echo "OCR alphanumeric chars: $char_count"
# Threshold rationale: the macOS menubar (Apple logo,
# "Finder", clock, date) alone OCRs to ~30+ chars on a healthy
# session. <10 chars almost always means rendering is broken
# - either the screen is black or every glyph rendered as a
# blank cell, which is the c3e2331 failure mode.
if [ "$char_count" -lt 10 ]; then
echo "::warning::OCR found <10 alphanumeric chars. Visual rendering may be broken - inspect screenshot.png artifact."
fi
- name: Upload visual smoke artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: macos_render_smoke
path: |
screenshot.png
paneflow.log
if-no-files-found: warn
retention-days: 7
# ───────────────────────────────────────────────────────────────────────
# Single aggregator job - mirrors Zed's run_tests.yml `tests_pass`
# pattern. Every remaining job is listed here; the step body iterates
# over each result and fails the aggregator if any non-skipped job
# came back as anything other than "success".
#
# Why this exists (vs. listing every job individually in branch
# protection): when CI grows or shrinks, only this `needs:` list and
# the `check_result` block below need editing. The required-status
# check in branch protection stays pointed at `tests_pass` forever,
# so PRs don't get stuck on stale required-check names after a
# workflow refactor. `if: always()` ensures the aggregator still
# reports even when upstream jobs fail, so the merge gate can see
# the red.
#
# `security_audit` is INCLUDED in the gate (since 2026-05-27). Its
# warn-vs-deny graduation lives in deny.toml, not in CI flags, so
# a green job means "no unignored advisories, all licenses
# allow-listed, all sources accepted" - which is exactly what we
# want to enforce.
tests_pass:
name: tests_pass
needs:
- orchestrate
- platform_census
- macos_check
- macos_render_smoke
- security_audit
if: always()
runs-on: macos-15
steps:
- name: Aggregate upstream results
env:
RESULT_ORCHESTRATE: ${{ needs.orchestrate.result }}
RESULT_PLATFORM_CENSUS: ${{ needs.platform_census.result }}
RESULT_MACOS_CHECK: ${{ needs.macos_check.result }}
RESULT_MACOS_RENDER_SMOKE: ${{ needs.macos_render_smoke.result }}
RESULT_SECURITY_AUDIT: ${{ needs.security_audit.result }}
run: |
set +x
EXIT_CODE=0
check_result() {
echo "* $1: $2"
if [[ "$2" != "skipped" && "$2" != "success" ]]; then
EXIT_CODE=1
fi
}
check_result "orchestrate" "$RESULT_ORCHESTRATE"
check_result "platform_census" "$RESULT_PLATFORM_CENSUS"
check_result "macos_check" "$RESULT_MACOS_CHECK"
check_result "macos_render_smoke" "$RESULT_MACOS_RENDER_SMOKE"
check_result "security_audit" "$RESULT_SECURITY_AUDIT"
exit $EXIT_CODE