306 lines
14 KiB
YAML
306 lines
14 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
RUSTFLAGS: "-D warnings"
|
|
|
|
# Cancel a pull request's superseded runs instead of letting them burn
|
|
# to completion. This project rebases heavily — the ledger re-conflicts
|
|
# on nearly every merge — so a branch routinely takes several pushes
|
|
# while an earlier run is still going, and each of those runs is
|
|
# obsolete the moment the next push lands. macOS minutes are the
|
|
# expensive ones and the macOS leg is the critical path, so superseded
|
|
# runs are exactly where the waste concentrates.
|
|
#
|
|
# Scoped to pull requests deliberately. `github.event.pull_request.number`
|
|
# is empty for a push to `main`, so the fallback keys those runs by SHA:
|
|
# every `main` commit gets its own group and none can cancel another.
|
|
# Cancelling a `main` run would leave the branch-protection record
|
|
# ambiguous about a commit that has already landed — the one place this
|
|
# saving is not worth having.
|
|
concurrency:
|
|
group: ci-${{ github.event.pull_request.number || github.sha }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
# Every job carries `timeout-minutes`. Without one a job inherits
|
|
# GitHub's 360-minute default, so a single hung test burns six hours —
|
|
# times four on the test matrix — and reports nothing useful at the end
|
|
# of it.
|
|
#
|
|
# The ceilings are justified against OBSERVED EXECUTION, and the
|
|
# numbers are a reading rather than a constant, so re-measure before
|
|
# trusting them:
|
|
#
|
|
# * observed max, 25-run window: 17 min (macOS/luajit)
|
|
# * observed max, 12-run window: 15.8 min (same job)
|
|
# * every other job: under 4 min
|
|
#
|
|
# `timeout-minutes` counts EXECUTION, not queue time — a 33-minute
|
|
# wall-clock run in that window executed its longest job in 17 — so no
|
|
# run in the observed history would have been killed by these values.
|
|
#
|
|
# The exposure is the case the window does NOT contain: a COLD CACHE.
|
|
# A stable-toolchain bump invalidates Swatinem's key on every leg at
|
|
# once, and a cold macOS debug build of this workspace plus the suite is
|
|
# the plausible way a HEALTHY run exceeds its ceiling. The test job
|
|
# therefore gets 35 rather than 25 — roughly 2x its observed max — while
|
|
# everything else keeps 25 against a sub-4-minute observed max, except
|
|
# `m6-perf-gates`, which keeps its own tighter 15.
|
|
#
|
|
# DIAGNOSIS, WRITTEN BEFORE IT HAPPENS: four test legs timing out
|
|
# simultaneously, shortly after a Rust release, is a cold cache and not
|
|
# a hang. Rerun, or raise this number. A single leg timing out while its
|
|
# siblings pass is the hang case these ceilings exist to catch.
|
|
#
|
|
# This is also the gate that has to exist before the basedpyright-class
|
|
# hang can ever be armed — see `PMACS_REQUIRE_PYRIGHT`, deliberately
|
|
# never set, in the test job below. 35 still beats the 360-minute
|
|
# default by an order of magnitude.
|
|
jobs:
|
|
fmt:
|
|
name: Format
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
components: rustfmt
|
|
- run: cargo fmt --all --check
|
|
|
|
clippy:
|
|
name: Lint (${{ matrix.lua }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# mlua's Lua-flavor features are mutually exclusive, so we can't
|
|
# use --all-features. Run clippy under each supported flavor.
|
|
lua: [luajit, lua54]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
components: clippy
|
|
- uses: Swatinem/rust-cache@v2
|
|
- run: cargo clippy --all-targets --no-default-features --features ${{ matrix.lua }} -- -D warnings
|
|
# pmacs-gpu is Lua-flavor-independent (it depends on pmacs-protocol,
|
|
# not pmacs), so the root-package clippy above never lints it. Lint
|
|
# it explicitly or its warnings slip through CI (audit F-001).
|
|
- run: cargo clippy -p pmacs-gpu --all-targets -- -D warnings
|
|
# pmacs-protocol is likewise never linted by the root-package
|
|
# clippy above: the workspace default member is only `pmacs`. The
|
|
# local `--workspace` gate covers it, so it passes today — CI has
|
|
# simply never checked, and a warning introduced through a
|
|
# protocol-only PR would reach `main` unseen.
|
|
- run: cargo clippy -p pmacs-protocol --all-targets -- -D warnings
|
|
|
|
gpu-render:
|
|
name: GPU Render (headless)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- uses: Swatinem/rust-cache@v2
|
|
# The headless render harness (audit F-014) needs a Vulkan adapter;
|
|
# the runner has no GPU, so install the mesa software rasterizer
|
|
# (lavapipe). This job also runs pmacs-gpu's other tests, which the
|
|
# workspace test job (root package only) never executes.
|
|
- name: Install lavapipe (Vulkan software rasterizer)
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y mesa-vulkan-drivers vulkan-tools
|
|
- name: Confirm a Vulkan adapter is present
|
|
run: |
|
|
ls -la /usr/share/vulkan/icd.d/ || true
|
|
vulkaninfo --summary || true
|
|
- name: pmacs-gpu tests (render harness included)
|
|
env:
|
|
# Force the Vulkan backend so wgpu uses lavapipe. Do NOT set
|
|
# VK_ICD_FILENAMES — the loader's default ICD discovery already
|
|
# finds lavapipe (vulkaninfo above proves it), and pinning a
|
|
# path that doesn't match the runner hides every ICD. The
|
|
# PMACS_REQUIRE_GPU guard turns a missing adapter into a hard
|
|
# failure so a broken setup can't pass as a silent skip.
|
|
WGPU_BACKEND: vulkan
|
|
LIBGL_ALWAYS_SOFTWARE: "1"
|
|
PMACS_REQUIRE_GPU: "1"
|
|
run: cargo test -p pmacs-gpu -- --test-threads=1
|
|
|
|
test:
|
|
name: Test (${{ matrix.os }} / ${{ matrix.lua }})
|
|
runs-on: ${{ matrix.os }}
|
|
# 35, not 25: this is the only job whose observed max is minutes
|
|
# rather than seconds, and the only one a cold cache can plausibly
|
|
# push past a 25-minute ceiling on all four legs at once. See the
|
|
# note above `jobs:` for the measurements and the cold-cache
|
|
# diagnosis.
|
|
timeout-minutes: 35
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest]
|
|
lua: [luajit, lua54]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- uses: Swatinem/rust-cache@v2
|
|
# Arm the external-tool-gated tests (see `TEST_IMPROVEMENT.md`
|
|
# §1.2). Before this step nothing installed these tools, so every
|
|
# test guarded on them returned early and reported GREEN without
|
|
# executing its body — a whole block of real-language-server and
|
|
# multi-shell coverage that had never once run in CI. Installing
|
|
# them is only half the fix; the `PMACS_REQUIRE_*` variables below
|
|
# are what turn a future missing tool back into a failure instead
|
|
# of silently restoring the vacuum.
|
|
#
|
|
# Linux only for now, deliberately. macOS would need the brew
|
|
# equivalents and roughly doubles the install cost on the slowest
|
|
# leg of the matrix; arming one platform already converts these
|
|
# from never-executed to executed, and the second is incremental.
|
|
# The tests still skip cleanly on macOS because the variables are
|
|
# unset there.
|
|
- name: Install external tools that gate acceptance tests (Linux)
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y clangd zsh fish lua5.4
|
|
# `locate_lua` looks for `lua` or `luajit` by name; the
|
|
# distro package installs `lua5.4` only.
|
|
sudo ln -sf "$(command -v lua5.4)" /usr/local/bin/lua
|
|
# rust-analyzer belongs HERE, not on the shared toolchain
|
|
# step. `components:` there applies to every matrix leg, so it
|
|
# would install the binary on macOS too — and *presence*, not
|
|
# PMACS_REQUIRE_LSP, is what decides whether a gated test body
|
|
# runs. That would have executed the rust-analyzer tests on
|
|
# macOS for the first time ever, on the legs that are both the
|
|
# CI critical path and the documented flake surface, while
|
|
# this lane's text claimed Linux only.
|
|
rustup component add rust-analyzer
|
|
# Versions are PINNED. `@latest` and bare `npm install -g`
|
|
# make CI behaviour drift with upstream releases: a bad gopls
|
|
# or yaml-language-server publish then breaks CI with no
|
|
# commit in this repository to bisect against.
|
|
go install golang.org/x/tools/gopls@v0.16.2
|
|
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
|
npm install -g vscode-langservers-extracted@4.10.0 \
|
|
yaml-language-server@1.15.0
|
|
- run: cargo build --all-targets --no-default-features --features ${{ matrix.lua }}
|
|
# Several acceptance binaries spawn real daemon / PTY child
|
|
# processes. Keep the harness serial so macOS runners do not
|
|
# expose cross-test process lifecycle races that are unrelated
|
|
# to the behavior under test.
|
|
#
|
|
# PMACS_REQUIRE_* make a missing tool fatal rather than a silent
|
|
# skip, exactly as PMACS_REQUIRE_GPU already does for the headless
|
|
# render job. Set only where the install step ran.
|
|
#
|
|
# PMACS_REQUIRE_PYRIGHT is deliberately NOT set and basedpyright
|
|
# is deliberately NOT installed. Both original reasons are now
|
|
# gone: the hang's root cause was the stdin-field drop ordering in
|
|
# `RuntimeHandles::drop` and is fixed, and this job now carries
|
|
# `timeout-minutes`, so a hang could no longer burn six hours.
|
|
# The ONE remaining reason is the plain one --- basedpyright is not
|
|
# installed here, so arming the variable would fail rather than
|
|
# test anything. Installing it (a uv + bundled-node download on
|
|
# every leg) is its own decision, not a rider on the hang fix.
|
|
#
|
|
# PMACS_REQUIRE_SETSID arms the teardown-deadlock unit test. Its
|
|
# fixture orphans a grandchild with `setsid --fork`, which is
|
|
# util-linux rather than coreutils, so the test skips when the
|
|
# binary is absent (a minimal container must not fail `--lib`
|
|
# without ever testing pmacs) and this variable is what makes the
|
|
# skip fatal where the tool is guaranteed.
|
|
- run: cargo test --all-targets --no-default-features --features ${{ matrix.lua }} -- --test-threads=1
|
|
env:
|
|
PMACS_REQUIRE_LSP: ${{ runner.os == 'Linux' && '1' || '' }}
|
|
PMACS_REQUIRE_SHELLS: ${{ runner.os == 'Linux' && '1' || '' }}
|
|
PMACS_REQUIRE_LUA: ${{ runner.os == 'Linux' && '1' || '' }}
|
|
PMACS_REQUIRE_SETSID: ${{ runner.os == 'Linux' && '1' || '' }}
|
|
- run: cargo test --doc --no-default-features --features ${{ matrix.lua }}
|
|
# The workspace default member is only the root `pmacs` package, so
|
|
# the runs above never execute pmacs-protocol's own tests — the
|
|
# shared wire format (encode/decode, transport framing) the daemon,
|
|
# TUI, and GPU all depend on. Run them explicitly (Lua-flavor
|
|
# independent, so once is enough; runs on every matrix leg cheaply
|
|
# off the shared cache).
|
|
- run: cargo test -p pmacs-protocol --all-targets
|
|
|
|
acceptance:
|
|
name: M1 Acceptance Gates
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- uses: Swatinem/rust-cache@v2
|
|
- name: perf, memory, and 30s fuzz gates
|
|
run: cargo test --release --test acceptance -- --ignored --nocapture --test-threads=1
|
|
|
|
m4-perf-gates:
|
|
name: M4 Perf Gates
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- uses: Swatinem/rust-cache@v2
|
|
- name: tree-sitter parse + highlight latency budgets
|
|
run: cargo test --release --test m4_acceptance -- --ignored --nocapture --test-threads=1
|
|
|
|
# m5-perf-gates: luajit-only by design.
|
|
# The keystroke-to-render gate measures protocol-path latency (Unix
|
|
# socket frontend ↔ daemon RTT plus the daemon's keymap-dispatch /
|
|
# buffer-mutate / repaint pipeline). Lua flavor is irrelevant — the
|
|
# measured path doesn't enter the Lua VM. Matrixing over flavors
|
|
# doubles CI cost without adding signal. If a future perf gate
|
|
# measures Lua-touching code paths (command dispatch via Lua,
|
|
# hook invocation), revisit then.
|
|
m5-perf-gates:
|
|
name: M5 Perf Gates
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- uses: Swatinem/rust-cache@v2
|
|
- name: keystroke-to-render p99 over loopback LocalSocket
|
|
run: cargo test --release --test m5_perf_acceptance -- --ignored --nocapture
|
|
|
|
# m6-perf-gates: luajit-only by design. The M6.6/M6.7 gates measure
|
|
# rope-append throughput, RSS ceiling, cancel-response latency,
|
|
# scrollback navigation latency, and scrollback search latency —
|
|
# none of which exercise the Lua VM's hot path differently across
|
|
# flavors. Same reasoning as m5-perf-gates above.
|
|
#
|
|
# --test-threads=1 is required: the M6.6 RSS-ceiling gate samples
|
|
# process-wide /proc/self/status, and concurrent ingest / scrollback
|
|
# populate from sibling tests inflate that reading. Serializing
|
|
# gives each test a clean baseline. The wall-clock cost of serial
|
|
# execution (~5 minutes) is acceptable for a perf-gates job that
|
|
# only runs on push/PR.
|
|
m6-perf-gates:
|
|
name: M6 Perf Gates
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- uses: Swatinem/rust-cache@v2
|
|
- name: ingest rate, RSS ceiling, cancel p99, navigation p99, search p99
|
|
env:
|
|
# Full cancel profile remains the test default. Hosted CI has
|
|
# an effective five-minute ceiling for this job and variable
|
|
# PTY throughput, so keep the per-PR profile short and stable.
|
|
PMACS_M6_INGEST_MIN_BYTES_PER_SEC: "67108864"
|
|
PMACS_M6_CANCEL_TRIALS: "30"
|
|
PMACS_M6_CANCEL_MAX_DELAY_MS: "500"
|
|
run: cargo test --release --test m6_perf_acceptance -- --ignored --nocapture --test-threads=1
|