Engine track E1-E3: render-state dirty tracking + the measurement that proves it #1008
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: CI | |
| # CI for Roost (Rust + Swift). Runs on main + PRs targeting main. A | |
| # `changes` job (path filter) gates the rust/swift/gtk jobs so they run only when | |
| # code that impacts them changes; `ci-success` is the single aggregated required | |
| # check (stable regardless of which jobs run). | |
| on: | |
| push: | |
| branches: [main, poc/iced] | |
| pull_request: | |
| branches: [main, poc/iced] | |
| permissions: | |
| contents: read | |
| jobs: | |
| changes: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| outputs: | |
| rust: ${{ steps.filter.outputs.rust }} | |
| mac: ${{ steps.filter.outputs.mac }} | |
| linux: ${{ steps.filter.outputs.linux }} | |
| fixtures: ${{ steps.filter.outputs.fixtures }} | |
| tests: ${{ steps.filter.outputs.tests }} | |
| ci: ${{ steps.filter.outputs.ci }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - uses: dorny/paths-filter@v4 | |
| id: filter | |
| with: | |
| filters: | | |
| rustcore: &rustcore | |
| - 'Cargo.toml' | |
| - 'Cargo.lock' | |
| - 'rust-toolchain.toml' | |
| - 'mise.toml' | |
| - 'clippy.toml' | |
| - 'crates/**' | |
| - 'third_party/ghostty/**' | |
| - 'third_party/swash/**' | |
| rust: | |
| - *rustcore | |
| mac: | |
| - *rustcore | |
| - 'mac/**' | |
| linux: | |
| - *rustcore | |
| - 'linux/**' | |
| # repo-root shared fixture corpora (parity gates for both UIs), | |
| # consumed only by the unit-test jobs (rust-build, gtk-build, | |
| # swift-mac) — distinct from the `tests` output below, which | |
| # means tools/roosttest/** | |
| fixtures: | |
| - 'tests/**' | |
| tests: | |
| - 'tools/roosttest/**' | |
| - 'tools/roosttest_unit/**' | |
| - 'tools/input/linux/**' | |
| - 'tools/wayland/**' | |
| - 'pyproject.toml' | |
| - 'uv.lock' | |
| ci: | |
| - '.github/workflows/ci.yml' | |
| rust-lint: | |
| needs: changes | |
| if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.ci == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install Rust toolchain (from rust-toolchain.toml) | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| - name: Cache cargo registry + target | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-lint-${{ runner.os }}-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} | |
| restore-keys: cargo-lint-${{ runner.os }}- | |
| - name: cargo fmt --check | |
| run: cargo fmt --all -- --check | |
| - name: cargo clippy | |
| # UI crates have native dependencies and dedicated strict lint jobs. | |
| # Keep this runner the toolkit-neutral core lane. | |
| run: cargo clippy --workspace --exclude roost-linux --exclude roost-iced --all-targets -- -D warnings | |
| harness-unit: | |
| needs: changes | |
| if: needs.changes.outputs.tests == 'true' || needs.changes.outputs.ci == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Functional harness target contract | |
| run: python3 -m unittest discover -s tools/roosttest_unit -v | |
| themes-parity: | |
| needs: changes | |
| if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.mac == 'true' || needs.changes.outputs.ci == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| # The bundled themes live in two trees (Rust crate + Mac SwiftPM | |
| # bundle) because SwiftPM `.copy` can't reach outside mac/. Guard | |
| # the copies against drift — adding a theme to one UI only is a | |
| # classic miss. | |
| - name: Bundled themes byte-identical across UIs | |
| run: diff -r crates/roost-ui-model/src/resources/themes mac/Sources/Roost/Resources/themes | |
| rust-build: | |
| needs: changes | |
| if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.fixtures == 'true' || needs.changes.outputs.ci == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install libclang (Linux) | |
| if: runner.os == 'Linux' | |
| # libclang-dev is needed by bindgen when roost-vt is built with | |
| # `--features ffi`. macOS gets it via Xcode's command-line tools. | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libclang-dev | |
| - name: Install Rust toolchain (from rust-toolchain.toml) | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| - uses: jdx/mise-action@v4 | |
| - name: Cache vendored libghostty-vt | |
| id: cache-ghostty | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| third_party/ghostty/out | |
| third_party/ghostty/src | |
| key: vendored-ghostty-${{ runner.os }}-${{ hashFiles('third_party/ghostty/build.sh') }} | |
| - name: Build libghostty-vt | |
| if: steps.cache-ghostty.outputs.cache-hit != 'true' | |
| run: ./third_party/ghostty/build.sh | |
| - name: Cache cargo registry + target | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} | |
| restore-keys: cargo-${{ runner.os }}- | |
| - name: cargo build (workspace, default features) | |
| # Default features: roost-vt's `ffi` feature is OFF, so this build does | |
| # not consume libghostty-vt. `--exclude roost-linux` (GTK toolchain | |
| # lives in gtk-build). | |
| run: cargo build --workspace --exclude roost-linux --all-targets | |
| - name: cargo build (roost-vt with ffi) | |
| run: cargo build -p roost-vt --features ffi | |
| # The encoder regression tests (key + mouse) live behind the `ffi` | |
| # feature because they exercise libghostty-vt directly; the default | |
| # `cargo test` below can't see them. The archive is built above, so | |
| # run them here as a real gate. | |
| - name: cargo test (roost-vt with ffi) | |
| run: cargo test -p roost-vt --features ffi | |
| - name: cargo test | |
| run: cargo test --workspace --exclude roost-linux | |
| # The experimental Swift-facing facade is feature-gated with no | |
| # production consumer; test it explicitly so the gated code stays green. | |
| - name: cargo test (roost-engine with facade) | |
| run: cargo test -p roost-engine --features facade | |
| swift-mac: | |
| needs: changes | |
| if: needs.changes.outputs.mac == 'true' || needs.changes.outputs.fixtures == 'true' || needs.changes.outputs.ci == 'true' | |
| runs-on: macos-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Show Swift toolchain | |
| run: swift --version | |
| - uses: jdx/mise-action@v4 | |
| - name: Cache vendored libghostty-vt | |
| id: cache-ghostty | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| third_party/ghostty/out | |
| third_party/ghostty/src | |
| key: vendored-ghostty-${{ runner.os }}-${{ hashFiles('third_party/ghostty/build.sh') }} | |
| - name: Build libghostty-vt | |
| if: steps.cache-ghostty.outputs.cache-hit != 'true' | |
| run: ./third_party/ghostty/build.sh | |
| - name: Cache SwiftPM artifacts | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| mac/.build | |
| ~/Library/Caches/org.swift.swiftpm | |
| key: swiftpm-${{ runner.os }}-${{ hashFiles('mac/Package.swift', 'mac/Package.resolved') }} | |
| restore-keys: swiftpm-${{ runner.os }}- | |
| - name: Cache cargo registry + target (for embedded roost-cli build) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-${{ runner.os }}-swiftmac-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} | |
| restore-keys: cargo-${{ runner.os }}-swiftmac- | |
| - name: swift build | |
| working-directory: mac | |
| run: swift build -v | |
| - name: Assert no dynamic ghostty-vt link (regression guard) | |
| working-directory: mac | |
| run: | | |
| # shellcheck disable=SC2044 # binary names are fixed ('Roost'/'RoostPackageTests'), no spaces | |
| for bin in $(find .build -type f -perm -u+x -name 'Roost' -o -name 'RoostPackageTests'); do | |
| if otool -L "$bin" 2>/dev/null | grep -q '@rpath/libghostty-vt'; then | |
| echo "FAIL: $bin is dynamically linked to libghostty-vt; reintroduces the dyld @rpath bug fixed in Package.swift's positional-archive linkerSettings." | |
| otool -L "$bin" | |
| exit 1 | |
| fi | |
| done | |
| - name: swift test | |
| working-directory: mac | |
| run: swift test | |
| - name: bundle .app (release) + embed roostctl | |
| run: ./mac/scripts/bundle.sh release | |
| - name: Verify embedded roostctl | |
| run: | | |
| set -euo pipefail | |
| BIN="mac/build/Roost.app/Contents/Resources/bin/roostctl" | |
| test -x "$BIN" || { echo "FAIL: $BIN not embedded"; exit 1; } | |
| "$BIN" --version | |
| codesign -dvv "$BIN" 2>&1 | grep -E 'adhoc|Format=' || true | |
| - name: Verify themes resource bundle shipped (clean-install guard) | |
| run: | | |
| set -euo pipefail | |
| # v0.0.2 crashed on every clean install because the themes bundle | |
| # wasn't where the running app looks. Assert it ships under | |
| # Contents/Resources, which is where `Bundle.roostResources` | |
| # (Theme.swift) resolves it. A deterministic file check — stronger | |
| # and faster than inferring from a launch. GUI-launch coverage is | |
| # the required e2e-mac job; the clean-install live launch is the | |
| # local `make smoke-mac-launch` / pre-release step. | |
| THEMES="mac/build/Roost.app/Contents/Resources/Roost_Roost.bundle/themes" | |
| test -d "$THEMES" || { echo "FAIL: $THEMES missing — themes can't load on a clean install (the v0.0.2 crash)"; exit 1; } | |
| count=$(find "$THEMES" -type f | wc -l | tr -d ' ') | |
| [ "$count" -gt 0 ] || { echo "FAIL: $THEMES shipped empty"; exit 1; } | |
| echo "OK: $count theme files shipped under Contents/Resources" | |
| - name: Verify TCC capture entitlements + usage strings (signed bundle) | |
| run: | | |
| set -euo pipefail | |
| # The signed bundle is the source of truth: the EntitlementsTests | |
| # swift test guards the source templates, but only this proves | |
| # bundle.sh actually signed the keys in. Without the capture | |
| # entitlements, programs hosted in a Roost tab lose mic/camera/ | |
| # apple-events access *silently* under the hardened runtime. | |
| APP="mac/build/Roost.app" | |
| ROOSTCTL="$APP/Contents/Resources/bin/roostctl" | |
| INFO="$APP/Contents/Info.plist" | |
| work=$(mktemp -d) | |
| trap 'rm -rf "$work"' EXIT | |
| # Extract each binary's entitlements as a real plist. codesign's text | |
| # dump format is unstable across macOS versions (XML on some, a | |
| # [Key] listing on others), so we force --xml and parse with | |
| # PlistBuddy for exact, substring-proof key lookup. | |
| app_ent="$work/app.plist"; ctl_ent="$work/roostctl.plist" | |
| # No `|| true`: a failed extraction (unsigned/unreadable binary) must | |
| # fail the job — otherwise the negative-only roostctl check below | |
| # would pass vacuously on an empty plist. | |
| codesign -d --entitlements - --xml "$APP" 2>/dev/null > "$app_ent" | |
| codesign -d --entitlements - --xml "$ROOSTCTL" 2>/dev/null > "$ctl_ent" | |
| has_key() { /usr/libexec/PlistBuddy -c "Print :$2" "$1" >/dev/null 2>&1; } | |
| plist_value() { /usr/libexec/PlistBuddy -c "Print :$2" "$1" 2>/dev/null || true; } | |
| # Each takes: $1=label $2=plist; remaining args=exact key names. | |
| require_true() { # key must exist AND be boolean true (not just present) | |
| local label="$1" plist="$2"; shift 2 | |
| for key in "$@"; do | |
| [ "$(plist_value "$plist" "$key")" = "true" ] \ | |
| || { echo "FAIL: $label must carry $key=true"; exit 1; } | |
| done | |
| } | |
| require_nonempty() { # key must exist AND be a non-whitespace string | |
| local label="$1" plist="$2"; shift 2 | |
| for key in "$@"; do | |
| [ -n "$(plist_value "$plist" "$key" | tr -d '[:space:]')" ] \ | |
| || { echo "FAIL: $label missing non-empty $key"; exit 1; } | |
| done | |
| } | |
| forbid_keys() { # key must be absent | |
| local label="$1" plist="$2"; shift 2 | |
| for key in "$@"; do | |
| if has_key "$plist" "$key"; then | |
| echo "FAIL: $label unexpectedly carries $key"; exit 1 | |
| fi | |
| done | |
| } | |
| CAPTURE=( | |
| com.apple.security.device.audio-input | |
| com.apple.security.device.camera | |
| com.apple.security.automation.apple-events | |
| ) | |
| # Broad / unneeded keys kept out of the app (blast-radius control). | |
| BROAD=( | |
| com.apple.security.personal-information.addressbook | |
| com.apple.security.personal-information.calendars | |
| com.apple.security.personal-information.location | |
| com.apple.security.personal-information.photos-library | |
| com.apple.security.cs.allow-jit | |
| com.apple.security.cs.allow-unsigned-executable-memory | |
| com.apple.security.app-sandbox | |
| com.apple.security.network.client | |
| com.apple.security.network.server | |
| ) | |
| require_true "app" "$app_ent" "${CAPTURE[@]}" # app MUST carry the capture set (=true) | |
| forbid_keys "app" "$app_ent" "${BROAD[@]}" # app MUST NOT carry broad keys | |
| forbid_keys "roostctl" "$ctl_ent" "${CAPTURE[@]}" # helper MUST NOT inherit capture | |
| # The signed bundle's Info.plist MUST carry the paired purpose strings. | |
| require_nonempty "Info.plist" "$INFO" \ | |
| NSMicrophoneUsageDescription \ | |
| NSCameraUsageDescription \ | |
| NSAppleEventsUsageDescription | |
| echo "OK: capture entitlements (=true) + usage strings present; helper + broad keys excluded" | |
| gtk-build: | |
| needs: changes | |
| if: needs.changes.outputs.linux == 'true' || needs.changes.outputs.fixtures == 'true' || needs.changes.outputs.ci == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install GTK4 + libadwaita | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libgtk-4-dev \ | |
| libadwaita-1-dev \ | |
| pkg-config \ | |
| libclang-dev | |
| - name: Install Rust toolchain (from rust-toolchain.toml) | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| - uses: jdx/mise-action@v4 | |
| - name: Cache vendored libghostty-vt | |
| id: cache-ghostty | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| third_party/ghostty/out | |
| third_party/ghostty/src | |
| key: vendored-ghostty-${{ runner.os }}-${{ hashFiles('third_party/ghostty/build.sh') }} | |
| - name: Build libghostty-vt | |
| if: steps.cache-ghostty.outputs.cache-hit != 'true' | |
| run: ./third_party/ghostty/build.sh | |
| - name: Cache cargo registry + target | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} | |
| restore-keys: cargo-${{ runner.os }}- | |
| - name: cargo build -p roost-linux | |
| run: cargo build -p roost-linux | |
| # The rust-test job runs `--workspace --exclude roost-linux` (no GTK | |
| # toolchain there), so this is the ONLY place roost-linux's own tests | |
| # can run. Without it the crate's unit tests — including the key | |
| # encoder's Kitty-mode guards — compile but never execute anywhere. | |
| # Headless-safe: these tests never construct a widget or call gtk_init. | |
| - name: cargo test -p roost-linux | |
| run: cargo test -p roost-linux | |
| # roost-linux is clippy-clean (issue #283 closed out the last | |
| # type_complexity holdout), so this runs the same full gate as | |
| # rust-lint / Iced's "Test and lint Iced" step below — no more | |
| # `-A warnings` narrow denylist. disallowed_types (GtkDnD #236) and | |
| # disallowed_methods (raw grab_focus #234) are warn-by-default clippy | |
| # lints, so `-D warnings` still catches regressions on both; clippy.toml | |
| # stays the source of truth for which types/methods are disallowed. | |
| # rust-lint excludes roost-linux because it needs the GTK toolchain | |
| # that only this job has. | |
| - name: cargo clippy -p roost-linux | |
| run: cargo clippy -p roost-linux --all-targets -- -D warnings | |
| # Iced walking skeleton: exact released Iced + libghostty-vt on both host | |
| # platforms, with the common IPC harness driving a real PTY-backed window. | |
| # The Linux leg selects X11 under Xvfb; its wgpu renderer uses Mesa's | |
| # software Vulkan implementation when the runner exposes no physical GPU. | |
| iced-build-e2e: | |
| needs: changes | |
| if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.tests == 'true' || needs.changes.outputs.ci == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| renderer: [wgpu, tiny-skia] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install Iced native dependencies (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| fonts-noto-cjk libclang-dev libxkbcommon-x11-0 libwayland-client0 \ | |
| mesa-vulkan-drivers weston xvfb xdotool zsh | |
| - name: Install Rust toolchain (from rust-toolchain.toml) | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| - uses: jdx/mise-action@v4 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Cache vendored libghostty-vt | |
| id: cache-ghostty | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| third_party/ghostty/out | |
| third_party/ghostty/src | |
| key: vendored-ghostty-${{ runner.os }}-${{ hashFiles('third_party/ghostty/build.sh') }} | |
| - name: Build libghostty-vt | |
| if: steps.cache-ghostty.outputs.cache-hit != 'true' | |
| run: ./third_party/ghostty/build.sh | |
| - name: Cache cargo registry + Iced target | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-iced-${{ runner.os }}-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} | |
| restore-keys: cargo-iced-${{ runner.os }}- | |
| - name: Build Iced + roostctl | |
| run: cargo build -p roost-iced -p roost-cli | |
| - name: Test and lint Iced | |
| run: | | |
| cargo test -p roost-iced | |
| cargo clippy -p roost-iced --all-targets -- -D warnings | |
| - name: Verify toolkit dependency boundaries | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if cargo tree -p roost-engine | grep -E '(^| )(gtk4|libadwaita|iced) v'; then | |
| echo "FAIL: roost-engine depends on a UI toolkit" | |
| exit 1 | |
| fi | |
| if cargo tree -p roost-ui-model | grep -E '(^| )(gtk4|libadwaita|iced|pango|cairo-rs|wgpu) v'; then | |
| echo "FAIL: roost-ui-model depends on a UI toolkit or renderer" | |
| exit 1 | |
| fi | |
| if cargo tree -p roost-iced | grep -E '(^| )(gtk4|libadwaita|pango|cairo-rs|roost-linux) v'; then | |
| echo "FAIL: roost-iced depends on GTK or roost-linux" | |
| exit 1 | |
| fi | |
| - name: Run Iced functional E2E (Linux X11) | |
| if: runner.os == 'Linux' | |
| env: | |
| ICED_BACKEND: ${{ matrix.renderer }} | |
| RUST_LOG: warn | |
| ROOST_TEST_MODE: "1" | |
| ROOST_TEST_TIMEOUT_SCALE: "3" | |
| ROOST_E2E_ARTIFACT_DIR: ${{ runner.temp }}/roost-iced-e2e-x11-artifacts | |
| ROOST_E2E_LOG_DIR: ${{ runner.temp }}/roost-iced-e2e-x11-logs | |
| run: > | |
| xvfb-run -a --server-args="-screen 0 1920x1080x24" | |
| uv run --group test pytest | |
| tools/roosttest/test_smoke.py | |
| tools/roosttest/test_iced_walking_skeleton.py | |
| tools/roosttest/test_notifications.py | |
| tools/roosttest/test_provider.py | |
| tools/roosttest/test_sidebar_pixels.py | |
| tools/roosttest/test_tab_strip_pixels.py | |
| tools/roosttest/test_focus.py | |
| tools/roosttest/test_palette.py | |
| tools/roosttest/test_z_typography.py | |
| tools/roosttest/test_project_lifecycle.py | |
| tools/roosttest/test_sidebar_resize.py | |
| tools/roosttest/test_selection.py | |
| tools/roosttest/test_osc52.py | |
| --roost-target iced --roost-fresh -v | |
| - name: Run Iced real-input clipboard (Linux X11) | |
| if: runner.os == 'Linux' | |
| env: | |
| ICED_BACKEND: ${{ matrix.renderer }} | |
| RUST_LOG: warn | |
| ROOST_ICED_BIN: ${{ github.workspace }}/target/debug/roost-iced | |
| ROOST_REQUIRE_REAL_INPUT: "1" | |
| ROOST_TEST_TIMEOUT_SCALE: "3" | |
| ROOST_E2E_ARTIFACT_DIR: ${{ runner.temp }}/roost-iced-e2e-real-input-artifacts | |
| ROOST_E2E_LOG_DIR: ${{ runner.temp }}/roost-iced-e2e-real-input-logs | |
| run: python3 tools/input/linux/iced_clipboard_check.py | |
| - name: Run Iced functional E2E (Linux Wayland) | |
| if: runner.os == 'Linux' | |
| # weston's headless backend has no input seat. Iced 0.14's | |
| # smithay-clipboard correctly refuses wl_data_device ownership without | |
| # a focused seat/serial, so native clipboard coverage runs in the X11 | |
| # lane above. This lane still requires the complete non-clipboard | |
| # Wayland renderer suite; the POC plan records the real-seat evidence | |
| # and remaining programmatic-write limitation. | |
| env: | |
| ICED_BACKEND: ${{ matrix.renderer }} | |
| RUST_LOG: warn | |
| ROOST_TEST_MODE: "1" | |
| ROOST_TEST_TIMEOUT_SCALE: "3" | |
| ROOST_E2E_ARTIFACT_DIR: ${{ runner.temp }}/roost-iced-e2e-wayland-artifacts | |
| ROOST_E2E_LOG_DIR: ${{ runner.temp }}/roost-iced-e2e-wayland-logs | |
| run: > | |
| tools/wayland/weston-run.sh | |
| uv run --group test pytest | |
| tools/roosttest/test_smoke.py | |
| tools/roosttest/test_iced_walking_skeleton.py | |
| tools/roosttest/test_notifications.py | |
| tools/roosttest/test_provider.py | |
| tools/roosttest/test_sidebar_pixels.py | |
| tools/roosttest/test_tab_strip_pixels.py | |
| tools/roosttest/test_focus.py | |
| tools/roosttest/test_palette.py | |
| tools/roosttest/test_z_typography.py | |
| tools/roosttest/test_project_lifecycle.py | |
| tools/roosttest/test_sidebar_resize.py | |
| --roost-target iced --roost-fresh -v | |
| - name: Run Iced functional E2E (macOS) | |
| if: runner.os == 'macOS' | |
| env: | |
| ICED_BACKEND: ${{ matrix.renderer }} | |
| RUST_LOG: warn | |
| ROOST_TEST_MODE: "1" | |
| ROOST_TEST_TIMEOUT_SCALE: "3" | |
| ROOST_E2E_ARTIFACT_DIR: ${{ runner.temp }}/roost-iced-e2e-mac-artifacts | |
| ROOST_E2E_LOG_DIR: ${{ runner.temp }}/roost-iced-e2e-mac-logs | |
| run: > | |
| uv run --group test pytest | |
| tools/roosttest/test_smoke.py | |
| tools/roosttest/test_iced_walking_skeleton.py | |
| tools/roosttest/test_notifications.py | |
| tools/roosttest/test_provider.py | |
| tools/roosttest/test_sidebar_pixels.py | |
| tools/roosttest/test_tab_strip_pixels.py | |
| tools/roosttest/test_focus.py | |
| tools/roosttest/test_palette.py | |
| tools/roosttest/test_z_typography.py | |
| tools/roosttest/test_project_lifecycle.py | |
| tools/roosttest/test_sidebar_resize.py | |
| tools/roosttest/test_selection.py | |
| tools/roosttest/test_osc52.py | |
| --roost-target iced --roost-fresh -v | |
| - name: Collect Iced diagnostics | |
| if: always() | |
| shell: bash | |
| run: | | |
| mkdir -p diagnostics | |
| find "${RUNNER_TEMP}" -path '*/roost-iced-e2e-*-logs/*.log' \ | |
| -exec cp '{}' diagnostics/ \; 2>/dev/null || true | |
| while IFS= read -r screenshot; do | |
| suite="$(basename "$(dirname "${screenshot}")")" | |
| cp "${screenshot}" "diagnostics/${suite}-$(basename "${screenshot}")" | |
| done < <(find "${RUNNER_TEMP}" -path '*/roost-iced-e2e-*-artifacts/*.png' 2>/dev/null) | |
| if [ "${RUNNER_OS}" = "macOS" ]; then | |
| cp "$HOME"/Library/Logs/DiagnosticReports/roost-iced*.ips diagnostics/ 2>/dev/null || true | |
| fi | |
| ls -la diagnostics || true | |
| - name: Upload Iced diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: e2e-iced-${{ matrix.os }}-${{ matrix.renderer }}-diagnostics | |
| path: diagnostics/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| # Functional E2E: launch the real GTK UI headless under Xvfb and drive | |
| # it through the IPC op set with the pytest harness (tools/roosttest). | |
| # Required — Linux is the cheap, reliable headless target. | |
| e2e-gtk: | |
| needs: changes | |
| if: needs.changes.outputs.linux == 'true' || needs.changes.outputs.tests == 'true' || needs.changes.outputs.ci == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install GTK4 + libadwaita + Xvfb + zsh | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libgtk-4-dev libadwaita-1-dev pkg-config libclang-dev xvfb xdotool zsh | |
| - name: Install Rust toolchain (from rust-toolchain.toml) | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| - uses: jdx/mise-action@v4 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Cache vendored libghostty-vt | |
| id: cache-ghostty | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| third_party/ghostty/out | |
| third_party/ghostty/src | |
| key: vendored-ghostty-${{ runner.os }}-${{ hashFiles('third_party/ghostty/build.sh') }} | |
| - name: Build libghostty-vt | |
| if: steps.cache-ghostty.outputs.cache-hit != 'true' | |
| run: ./third_party/ghostty/build.sh | |
| - name: Cache cargo registry + target | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} | |
| restore-keys: cargo-${{ runner.os }}- | |
| # roostctl too: `test_agent_lifecycle.py` drives synthetic Claude | |
| # hook payloads through the real `roostctl claude-hook` binary, so | |
| # the adapter -> CLI -> IPC -> workspace path runs end to end. (The | |
| # harness can build it on demand, but that would land inside the | |
| # pytest step.) The Mac leg gets it from bundle.sh, which embeds | |
| # roostctl in the .app. | |
| - name: Build the GTK UI + roostctl | |
| run: cargo build -p roost-linux -p roost-cli | |
| - name: Run E2E (GTK under Xvfb) | |
| env: | |
| GDK_BACKEND: x11 | |
| RUST_LOG: warn | |
| ROOST_TEST_TIMEOUT_SCALE: "3" # shared CI runner is slower/variable | |
| # Unlocks tab.feed_pty_bytes + tab.capture_pty_input on the | |
| # UI side so the OSC-pipeline tests can drive PTY bytes | |
| # end-to-end. The harness reads the same var to skip those | |
| # tests entirely when it's absent (no spurious red). | |
| ROOST_TEST_MODE: "1" | |
| # Where the harness captures the launched UI's stdout+stderr (the | |
| # GTK UI tees its log to stdout). Collected + uploaded below so a | |
| # boot failure under xvfb isn't blind — the Mac twin already does | |
| # this via crash reports; this is the GTK equivalent. | |
| ROOST_E2E_LOG_DIR: ${{ runner.temp }}/roost-e2e-logs | |
| # The harness launches its own `roost` process; under xvfb-run it | |
| # inherits the virtual DISPLAY. tab.dump reads in-process render | |
| # state, so no compositor screen-capture is involved. | |
| # Big virtual screen so window.resize can grow the toplevel to the | |
| # full test width (the default 1280-wide xvfb capped resizes at | |
| # ~1270, registered-skipping the sidebar geometry tests). With room | |
| # to resize, those tests run instead of skipping. | |
| run: > | |
| xvfb-run -a --server-args="-screen 0 2560x1440x24" | |
| uv run --group test pytest tools/roosttest --roost-target gtk --roost-fresh -v | |
| # Real-input regressions: the behaviors that only real pointer/key input | |
| # through the GTK gesture/shortcut stack can exercise (the IPC suite drives | |
| # the op set, never the gesture stack) — click-to-focus + project-switch | |
| # core-sync (#1), Alt+digit / Ctrl+PageDown / cycle_tab / pill-click core- | |
| # sync (#228/#229), tab context-menu no-crash, and the GtkGestureDrag | |
| # tab/project reorder that replaced GTK DnD (whose Wayland drag-icon | |
| # surface aborted in gdksurface-wayland.c:frame_callback). Self-contained — | |
| # it starts its OWN Xvfb + throwaway Roost — so it runs as its own step, | |
| # NOT under xvfb-run. continue-on-error while we gather signal on XTEST- | |
| # under-Xvfb reliability on shared runners; promote to a required gate once | |
| # proven stable. ROOST_REQUIRE_REAL_INPUT turns the script's self-skip into | |
| # a failure here (xdotool/Xvfb are installed, so a skip means a real setup | |
| # problem, not "unsupported"). | |
| - name: Real-input regressions (Xvfb + xdotool) | |
| continue-on-error: true | |
| env: | |
| GDK_BACKEND: x11 | |
| ROOST_TEST_MODE: "1" | |
| ROOST_TEST_TIMEOUT_SCALE: "3" | |
| ROOST_REQUIRE_REAL_INPUT: "1" | |
| run: uv run --group test python tools/input/linux/real_input_check.py | |
| # Stage the captured UI log into one dir (upload-artifact doesn't | |
| # expand globs/`~`). The harness writes the launched UI's stdout+stderr | |
| # to ROOST_E2E_LOG_DIR; grab it whether or not the run failed. | |
| - name: Collect failure diagnostics | |
| if: always() | |
| run: | | |
| mkdir -p diagnostics | |
| cp "${RUNNER_TEMP}"/roost-e2e-logs/*.log diagnostics/ 2>/dev/null || true | |
| ls -la diagnostics || true | |
| - name: Upload E2E diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: e2e-gtk-diagnostics | |
| path: diagnostics/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| # Functional E2E against the GTK UI under a headless WAYLAND compositor | |
| # (weston) — the twin of e2e-gtk-under-Xvfb. Exists because GTK's | |
| # GDK-Wayland backend (gdksurface-wayland.c) is ONLY exercised here: the | |
| # Xvfb job forces GDK_BACKEND=x11, so Wayland-only bugs (e.g. the DnD | |
| # drag-icon-surface frame_callback abort that crashed tab reorder on | |
| # COSMIC) are invisible to it. | |
| # | |
| # NON-BLOCKING for now (continue-on-error + absent from ci-success) while | |
| # we triage the real-runner Wayland pass profile — Wayland clipboard + | |
| # selection semantics differ from X11, so a few OSC52/selection tests may | |
| # legitimately diverge. Promote into ci-success once the profile is clean | |
| # (mirrors the "real-click focus regression" gather-signal-then-promote | |
| # pattern above). weston headless drives the IPC suite, not pointer input; | |
| # the real pointer-DRAG guard lives in the separate e2e-gtk-wayland-drag job | |
| # (cage + /dev/uinput) below. | |
| e2e-gtk-wayland: | |
| needs: changes | |
| if: needs.changes.outputs.linux == 'true' || needs.changes.outputs.tests == 'true' || needs.changes.outputs.ci == 'true' | |
| runs-on: ubuntu-latest | |
| # Job-level cap so a hung run can't sprawl (a single hung test ate the 15m | |
| # step cap once → the job ran 22m). Sized to survive a cache-COLD run (cold | |
| # libghostty-vt + cargo build add ~10m), not the warm ~2m time. | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install GTK4 + libadwaita + weston + zsh | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libgtk-4-dev libadwaita-1-dev pkg-config libclang-dev weston wl-clipboard zsh | |
| - name: Install Rust toolchain (from rust-toolchain.toml) | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| - uses: jdx/mise-action@v4 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Cache vendored libghostty-vt | |
| id: cache-ghostty | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| third_party/ghostty/out | |
| third_party/ghostty/src | |
| key: vendored-ghostty-${{ runner.os }}-${{ hashFiles('third_party/ghostty/build.sh') }} | |
| - name: Build libghostty-vt | |
| if: steps.cache-ghostty.outputs.cache-hit != 'true' | |
| run: ./third_party/ghostty/build.sh | |
| - name: Cache cargo registry + target | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} | |
| restore-keys: cargo-${{ runner.os }}- | |
| # roostctl too — same reason as the Xvfb job above (the pytest | |
| # suite's Claude-hook test shells out to the real binary). | |
| - name: Build the GTK UI + roostctl | |
| run: cargo build -p roost-linux -p roost-cli | |
| - name: Run E2E (GTK under headless Wayland / weston) | |
| continue-on-error: true | |
| timeout-minutes: 15 | |
| env: | |
| RUST_LOG: warn | |
| ROOST_TEST_TIMEOUT_SCALE: "3" # shared CI runner is slower/variable | |
| ROOST_TEST_MODE: "1" | |
| ROOST_E2E_LOG_DIR: ${{ runner.temp }}/roost-e2e-wl-logs | |
| # weston-run.sh boots a headless weston, points GDK at it | |
| # (GDK_BACKEND=wayland) and runs the IPC-driven suite — tab.dump reads | |
| # in-process render state, so no screen capture is involved. | |
| # | |
| # Curated subset: skip the clipboard/selection + shell-env tests. On | |
| # headless Wayland there's no clipboard manager, so a GTK clipboard read | |
| # can block indefinitely (the full suite hung ~2h on the runner before | |
| # this cap), and those paths test integration semantics that diverge | |
| # X11↔Wayland — not the GDK-Wayland backend this job exists to cover. | |
| # `timeout-minutes` is the belt-and-suspenders cap so a future hang can | |
| # never run away again (continue-on-error swallows the timed-out step). | |
| # --timeout: per-test hard cap (pytest-timeout) so one hung test fails | |
| # fast instead of consuming the whole step (that's what ran the job to | |
| # 22m). Derived from the same ROOST_TEST_TIMEOUT_SCALE the harness uses | |
| # (30s base * 3 = 90s) so the one knob meant to absorb runner variance | |
| # doesn't get bypassed. `thread` method dumps the stack + fails without | |
| # relying on SIGALRM landing in a GTK/IPC C call. | |
| run: > | |
| tools/wayland/weston-run.sh | |
| uv run --group test pytest tools/roosttest --roost-target gtk --roost-fresh -v | |
| --timeout=$((30 * ROOST_TEST_TIMEOUT_SCALE)) --timeout-method=thread | |
| --ignore=tools/roosttest/test_osc52.py | |
| --ignore=tools/roosttest/test_selection.py | |
| --ignore=tools/roosttest/test_shell_integration.py | |
| - name: Collect failure diagnostics | |
| if: always() | |
| run: | | |
| mkdir -p diagnostics | |
| cp "${RUNNER_TEMP}"/roost-e2e-wl-logs/*.log diagnostics/ 2>/dev/null || true | |
| ls -la diagnostics || true | |
| - name: Upload E2E diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: e2e-gtk-wayland-diagnostics | |
| path: diagnostics/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| # Real pointer-DRAG under a headless WAYLAND compositor — the one thing the | |
| # IPC-only e2e-gtk-wayland job can't do. Drives an absolute-pointer drag with | |
| # the stdlib /dev/uinput injector (tools/input/linux/inject_pointer.py) under | |
| # `cage` (a kiosk wlroots compositor that fullscreens its single client, so | |
| # window coords == output coords). Guards that the GtkGestureDrag reorder | |
| # neither aborts the process nor emits a Wayland surface critical — the X11 | |
| # real-input job can only exercise the gesture *logic*, not the GDK-Wayland | |
| # backend where the old GtkDnD drag-icon surface crashed. | |
| # | |
| # NON-BLOCKING (continue-on-error + absent from ci-success): synthetic Wayland | |
| # input + the seat/libinput/uinput plumbing is new and may be flaky on shared | |
| # runners. The check SKIPs cleanly if a dep is missing and reports a clear | |
| # FAIL otherwise (ROOST_REQUIRE_REAL_INPUT=1). Promote to a gate once it has | |
| # green history on main (same gather-signal pattern as the jobs above). The | |
| # developer confirms the real COSMIC/cosmic-comp behavior separately; cage is | |
| # generic wlroots Wayland, which is where the crash lived. | |
| e2e-gtk-wayland-drag: | |
| needs: changes | |
| if: needs.changes.outputs.linux == 'true' || needs.changes.outputs.tests == 'true' || needs.changes.outputs.ci == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install GTK4 + libadwaita + cage + seatd | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libgtk-4-dev libadwaita-1-dev pkg-config libclang-dev cage seatd | |
| - name: Install Rust toolchain (from rust-toolchain.toml) | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| - uses: jdx/mise-action@v4 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Cache vendored libghostty-vt | |
| id: cache-ghostty | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| third_party/ghostty/out | |
| third_party/ghostty/src | |
| key: vendored-ghostty-${{ runner.os }}-${{ hashFiles('third_party/ghostty/build.sh') }} | |
| - name: Build libghostty-vt | |
| if: steps.cache-ghostty.outputs.cache-hit != 'true' | |
| run: ./third_party/ghostty/build.sh | |
| - name: Cache cargo registry + target | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} | |
| restore-keys: cargo-${{ runner.os }}- | |
| - name: Build the GTK UI + roostctl | |
| # roostctl (roost-cli) is what the injector harness drives; a cold | |
| # `-p roost-linux` build doesn't produce it, so build both explicitly. | |
| run: cargo build -p roost-linux -p roost-cli | |
| - name: Enable uinput + a seat for synthetic input | |
| # Headless wlroots has no input devices; to read the uinput device the | |
| # injector creates, cage needs a libinput backend on a seat. modprobe | |
| # uinput works on ubuntu-latest; seatd grants the seat to the runner's | |
| # group. Best-effort — the check SKIPs/FAILs clearly if this didn't take. | |
| run: | | |
| sudo modprobe uinput || true | |
| sudo chmod 0666 /dev/uinput || true | |
| sudo seatd -g "$(id -gn)" >"${RUNNER_TEMP}/seatd.log" 2>&1 & | |
| sleep 1 | |
| echo "LIBSEAT_BACKEND=seatd" >> "$GITHUB_ENV" | |
| - name: Wayland pointer-drag guard (cage + uinput) | |
| continue-on-error: true | |
| timeout-minutes: 12 | |
| env: | |
| ROOST_TEST_MODE: "1" | |
| ROOST_TEST_TIMEOUT_SCALE: "3" | |
| ROOST_REQUIRE_REAL_INPUT: "1" | |
| WLR_BACKENDS: "headless,libinput" | |
| WLR_RENDERER: "pixman" | |
| run: uv run --group test python tools/input/linux/wayland_drag_check.py | |
| # Functional E2E against the real Swift app, driven through the IPC op | |
| # set by the pytest harness (tools/roosttest) — the Mac twin of e2e-gtk. | |
| # Required (in ci-success). The harness clears any stale instance before | |
| # launch (tools/roosttest/ui.py) and timeouts scale up for the slower | |
| # shared runner; see docs/development/test-automation.md. | |
| e2e-mac: | |
| needs: changes | |
| if: needs.changes.outputs.mac == 'true' || needs.changes.outputs.tests == 'true' || needs.changes.outputs.ci == 'true' | |
| runs-on: macos-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - uses: jdx/mise-action@v4 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install modern bash + version probe | |
| # `test_bash_auto_bootstrap_tracks_cwd` needs bash >= 4.4 for its | |
| # `--posix` + ENV auto-bootstrap path; Apple's /bin/bash is 3.2, | |
| # SIP-locked. Brew lands modern bash at /opt/homebrew/bin/bash on | |
| # ARM runners, /usr/local/bin/bash on Intel (the test's | |
| # `_modern_bash()` probes both). The idempotent `list || install` | |
| # form is robust to runner images that ship bash preinstalled; | |
| # the version probe makes a future preinstall-regression loud in | |
| # CI logs rather than silently re-triggering the test's | |
| # `precondition("no modern bash")` failure. | |
| run: | | |
| brew list bash >/dev/null 2>&1 || brew install bash | |
| which bash || true | |
| for p in /opt/homebrew/bin/bash /usr/local/bin/bash; do | |
| if [ -x "$p" ]; then "$p" --version; fi | |
| done | |
| - name: Cache vendored libghostty-vt | |
| id: cache-ghostty | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| third_party/ghostty/out | |
| third_party/ghostty/src | |
| key: vendored-ghostty-${{ runner.os }}-${{ hashFiles('third_party/ghostty/build.sh') }} | |
| - name: Build libghostty-vt | |
| if: steps.cache-ghostty.outputs.cache-hit != 'true' | |
| run: ./third_party/ghostty/build.sh | |
| - name: Cache SwiftPM artifacts | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| mac/.build | |
| ~/Library/Caches/org.swift.swiftpm | |
| key: swiftpm-${{ runner.os }}-${{ hashFiles('mac/Package.swift', 'mac/Package.resolved') }} | |
| restore-keys: swiftpm-${{ runner.os }}- | |
| - name: Cache cargo registry + target | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-${{ runner.os }}-swiftmac-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} | |
| restore-keys: cargo-${{ runner.os }}-swiftmac- | |
| - name: Bundle Roost.app | |
| run: ./mac/scripts/bundle.sh debug | |
| - name: Run E2E (Mac app) | |
| env: | |
| ROOST_TEST_TIMEOUT_SCALE: "3" # shared macos-latest runner is slower/variable | |
| # Unlocks the test-only IPC ops in the bundled Mac app | |
| # (tools/roosttest/ui.py forwards this via `open --env`). | |
| # See the GTK job above for full rationale. | |
| ROOST_TEST_MODE: "1" | |
| # --roost-fresh: own a hermetic instance with an isolated, throwaway | |
| # ROOST_STATE_DIR (replaces the old ROOST_TEST_RESET_STATE clean-slate | |
| # hack — the harness no longer deletes the real state.json). | |
| run: > | |
| uv run --group test pytest tools/roosttest --roost-target mac --roost-fresh -v | |
| --junitxml=test-results/e2e-mac.xml | |
| # On failure, salvage what makes a red blocking check debuggable: the | |
| # app's own log, any macOS crash report, and a best-effort live | |
| # screenshot (only lands if the app is still up). Staged into one dir | |
| # because upload-artifact doesn't expand `~`/globs. | |
| - name: Collect failure diagnostics | |
| if: failure() | |
| run: | | |
| mkdir -p diagnostics | |
| cp "$HOME/Library/Logs/Roost/roost.log" diagnostics/ 2>/dev/null || true | |
| cp "$HOME"/Library/Logs/DiagnosticReports/Roost*.ips diagnostics/ 2>/dev/null || true | |
| cp "$HOME"/Library/Logs/DiagnosticReports/Roost*.crash diagnostics/ 2>/dev/null || true | |
| "mac/build/Roost.app/Contents/Resources/bin/roostctl" screenshot --out diagnostics/screen.png 2>/dev/null || true | |
| ls -la diagnostics || true | |
| - name: Upload E2E diagnostics + JUnit | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: e2e-mac-diagnostics | |
| path: | | |
| diagnostics/ | |
| test-results/ | |
| if-no-files-found: ignore | |
| # Single required check. Always runs; passes iff no gated job failed (skipped | |
| # jobs are fine — that's the path-filter doing its job). | |
| ci-success: | |
| needs: [changes, rust-lint, harness-unit, themes-parity, rust-build, swift-mac, gtk-build, iced-build-e2e, e2e-gtk, e2e-mac] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Verify no required job failed | |
| run: | | |
| results="${{ needs.rust-lint.result }} ${{ needs.harness-unit.result }} ${{ needs.themes-parity.result }} ${{ needs.rust-build.result }} ${{ needs.swift-mac.result }} ${{ needs.gtk-build.result }} ${{ needs.iced-build-e2e.result }} ${{ needs.e2e-gtk.result }} ${{ needs.e2e-mac.result }}" | |
| echo "job results: ${results}" | |
| # Allowlist, not a denylist. A denylist of failure/cancelled let | |
| # `abandoned` through — the status GitHub assigns when its own | |
| # infrastructure kills a job — so a run where 7 of 9 jobs never | |
| # executed reported this gate GREEN. Observed on PR #306 during a | |
| # GitHub "Failed to resolve action download info" incident. | |
| # Anything that is not an actual pass now fails the gate. | |
| for r in ${results}; do | |
| case "${r}" in | |
| success|skipped) ;; | |
| *) | |
| echo "::error::a required CI job did not pass: '${r}' (${results})" | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| echo "all good" |