fix(fork): prevent false Codex input alerts #208
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |