mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
* fix(control): bump the dialect to v6 so an out-of-date server says so The control dialect has been renamed, extended and cut since it was last numbered, all of it against CONTROL_VERSION 5: the machine tree replaced WorkspaceList/Get/Put/Delete with WorkspaceTree, MachineGet and the tab/pane verbs, GitStream arrived with its chunk and end events, and ReplyOk::Attached and FileMeta went away. A peer left behind by any of that still answers the hello, because the number it answers with still matches. It is also still sitting at the path the installer looks for, tty7-server-c5p5, so a client decides it already has the server it needs. Then the first call reaches a variant the peer has never heard of, the frame fails to decode, and the read loop takes the whole link down with it. What the user sees is a remote workspace that opens with no tabs and a git detail pane that never fills, with nothing anywhere saying why. Moving the number puts all three guards back: the hello is refused with the message that names the old build, the remote binary is looked for at c6p5 and installed rather than trusted, and a stale local daemon gets the restart prompt it should have been getting all along. Document the rule next to the constant while it is fresh: move it when a variant is added or removed. The feature strings only cover what a peer can safely ignore, and a request it cannot decode is not that. * feat(remote): publish a tty7-server for macOS hosts A remote workspace has been Linux-only for no reason anyone chose: the installer derives the asset name from `uname -sm`, and the only names it knew were the two musl builds. A Mac on the other end of an SSH profile got "a remote tty7 workspace needs a Linux host" and stopped there. Publish the two Apple slices alongside them and teach the installer to ask for them. `Darwin arm64` and `Darwin x86_64` now map to tty7-server-macos-aarch64 and tty7-server-macos-x86_64; everything past that point already worked, because nothing under it was ever Linux- specific — the install path is POSIX, the upload is SFTP, and the dialect probe runs the binary before trusting it. The machine names are matched per system rather than by architecture alone. Linux says aarch64 on one distribution and arm64 on the next, while a Mac only ever says arm64, so honouring Linux's spellings under Darwin would be guessing at output no Mac produces. Static linking is not the instrument on macOS — Apple ships no static libSystem — so assert-macho.sh stands in for assert-static.sh with the guarantee that actually matters: every dependency resolves under /usr/lib or /System/Library, so nothing the destination Mac lacks can be picked up from a build runner, and the binary carries the signature arm64 refuses to run without. Not signed or notarized beyond that, deliberately. The binary is never downloaded by the Mac that runs it: the client fetches it, verifies it against checksums.txt and writes it over SFTP, which sets no quarantine attribute, so Gatekeeper is not in the path. ASSET_X86_64 and ASSET_AARCH64 become ASSET_LINUX_*, which is what they always meant and could not keep meaning next to a macOS pair. * fix(ci): sign the x86_64 macOS server, and stop the guard flaking on it Two faults the first green run hid from each other. The linker ad-hoc signs the arm64 slice because Apple Silicon will not execute anything unsigned, and leaves x86_64 bare. That is fine on an Intel Mac, but the x86_64 server is also what an Apple Silicon box gets when it asks through a Rosetta shell, and handing that machine an unsigned binary is a guess about Rosetta nobody needs to make. Sign both slices ad-hoc in the workflow — no identity, no secrets, nothing to do with the notarized signing the GUI bundles get. The guard that caught it was itself unreliable: `codesign -dv | grep -q` under `pipefail` reports failure whenever grep wins the race, because -q exits on the first match and the writer takes SIGPIPE. Small output means the writer usually finishes first, which is why the arm64 job passed and x86_64 failed on the same signed-or-not question. Capture into a variable and match afterwards, the way the release workflow already does it. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
317 lines
14 KiB
YAML
317 lines
14 KiB
YAML
name: CI
|
|
|
|
# Compile + test on every push/PR. The Windows and Linux jobs are the
|
|
# compile-feedback loop for the platform-specific code a macOS dev machine
|
|
# never builds (`cfg(windows)` transport / process detach / config dir,
|
|
# the Linux `/proc` queries, the x11/wayland gpui backends). The macOS job
|
|
# guards against regressing the original target.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
# A superseded PR run is dead weight the moment the next push lands, and a run
|
|
# left going is not free: the account's concurrent-job budget is shared, and
|
|
# macOS slots are the scarce ones. A zombie Windows job (see the `Test` step's
|
|
# timeout below) once held a slot for the full six-hour job limit while an
|
|
# unrelated PR's macOS job queued behind it for two and a half hours.
|
|
#
|
|
# Pushes to main are exempt: each commit's own run is the record of whether that
|
|
# commit was green, and cancelling one to start the next would erase it.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
jobs:
|
|
fmt:
|
|
name: rustfmt
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
components: rustfmt
|
|
- run: cargo fmt --check
|
|
|
|
# `ui::` and `terminal::` must not reach the filesystem or git
|
|
# directly, because once a workspace can be remote those calls answer for the
|
|
# wrong machine. The allowlist of genuinely-local paths lives in the
|
|
# script, next to the reason each one is exempt.
|
|
#
|
|
# A standalone job on purpose, and one that must stay *non-required*: main's
|
|
# required checks are `rustfmt` and the three `build & test (<target>)` names,
|
|
# and folding this into either would make it required the moment it lands —
|
|
# wedging every open PR on a check they have never seen. Same reasoning as
|
|
# `server-musl` below. Cheap enough (a checkout and a grep) that it does not
|
|
# need caching or a toolchain.
|
|
host-boundary:
|
|
name: host boundary
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- run: bash .github/scripts/check-host-boundary.sh
|
|
|
|
build:
|
|
name: build & test (${{ matrix.target }})
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- runner: macos-14
|
|
target: aarch64-apple-darwin
|
|
- runner: windows-latest
|
|
target: x86_64-pc-windows-msvc
|
|
- runner: ubuntu-latest
|
|
target: x86_64-unknown-linux-gnu
|
|
runs-on: ${{ matrix.runner }}
|
|
# Backstop under the per-step timeouts below. Without any timeout at all a
|
|
# hung test runs to GitHub's six-hour job limit, which is how a 90-second
|
|
# step turned into a six-hour one three times in a day.
|
|
timeout-minutes: 60
|
|
steps:
|
|
- name: Checkout tty7
|
|
uses: actions/checkout@v4
|
|
|
|
# gpui-component is a git dependency (see Cargo.toml), so no sibling
|
|
# checkout is needed. The Windows backend (gpui_windows + DirectWrite/D3D)
|
|
# ships with the windows-latest runner's SDK — no extra system deps.
|
|
|
|
# gpui's Linux backends need the x11/wayland/xkb/font dev packages at
|
|
# build time (build scripts resolve them via pkg-config). Same set the
|
|
# README documents for building from source on Linux.
|
|
- name: Install Linux system dependencies
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y pkg-config cmake clang libxkbcommon-dev \
|
|
libxkbcommon-x11-dev libfontconfig1-dev libfreetype6-dev \
|
|
libwayland-dev libx11-dev libxcb1-dev libzstd-dev libssl-dev \
|
|
libkrb5-dev
|
|
echo "LIBGSSAPI_IMPL=mit" >> "$GITHUB_ENV"
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- uses: Swatinem/rust-cache@v2
|
|
|
|
# `--locked` on the build so a Cargo.lock that disagrees with Cargo.toml
|
|
# fails here instead of being silently rewritten. Without it the drift is
|
|
# invisible to CI and lands on contributors instead: every local `cargo`
|
|
# run rewrites the lock, leaving a permanently dirty working tree that has
|
|
# to be re-discarded before every commit. The release workflow locks its
|
|
# build too; only nightly stays unlocked, because it stamps Cargo.toml's
|
|
# version and relies on cargo refreshing the lock's own root entry.
|
|
- name: Build
|
|
# Cold-cache builds have been observed at ~10 min; warm ones at ~90s.
|
|
timeout-minutes: 30
|
|
run: cargo build --locked --target ${{ matrix.target }}
|
|
|
|
# `cargo test` has no timeout of its own, so one hung test is
|
|
# indistinguishable from a slow suite until the job hits GitHub's six-hour
|
|
# limit. The suite intermittently hangs here — roughly one run in ten, on
|
|
# any branch, while the same commit passes on a re-run — and every time it
|
|
# did, the job held a runner slot for hours and reported nothing.
|
|
#
|
|
# Seen on Windows three times and on Linux once, so it is not a
|
|
# platform-specific test: `Build` succeeds, `Test` starts, and nothing
|
|
# further is ever written.
|
|
#
|
|
# The step's honest budget is small: ~75s warm and ~3.5 min when this step
|
|
# also has to compile the test targets, on every platform. 20 minutes is
|
|
# pure headroom, so a trip means a hang, not a slow runner.
|
|
- name: Test
|
|
timeout-minutes: 20
|
|
run: cargo test --locked --target ${{ matrix.target }}
|
|
|
|
- name: Test desktop updater
|
|
if: runner.os == 'macOS' || runner.os == 'Windows'
|
|
timeout-minutes: 10
|
|
run: cargo test --locked --features updater --bin tty7-updater --target ${{ matrix.target }}
|
|
|
|
# There is deliberately no post-mortem step here, and that is worth
|
|
# recording, because an earlier version of this file had one: on failure it
|
|
# dumped the process table to find the surviving test binary.
|
|
#
|
|
# It does not work, and cannot. GitHub kills the step's whole process tree
|
|
# when `timeout-minutes` trips, *before* the next step runs, so on the one
|
|
# failure that matters the dump comes back empty — verified on run
|
|
# 30526538997, where it printed two headers and nothing between them.
|
|
#
|
|
# Nor is it needed. libtest already prints "<test> has been running for
|
|
# over 60 seconds" for a test that has not returned, and that line was
|
|
# sitting in every hung run all along. The obstacle was never the
|
|
# diagnostic: a job's log cannot be fetched while the job is in progress
|
|
# (the API 404s), and nobody cancels a six-hour run to read one. The `Test`
|
|
# timeout above is what fixes that — the step *fails*, so the log lands,
|
|
# with the name in it.
|
|
|
|
# Static musl builds of the headless server binary that remote workspaces push
|
|
# onto the far machine (decision D10).
|
|
# One binary has to run on any distro without regard to the target's glibc
|
|
# version, so it is linked fully static against musl rather than built per
|
|
# distro. Compile-only — this job publishes nothing; release.yml and
|
|
# nightly.yml carry the same job with an upload step.
|
|
#
|
|
# Deliberately a *separate* job rather than two more rows in the `build` matrix
|
|
# above: those three `build & test (<target>)` names are main's required
|
|
# checks, and reshaping that matrix would wedge branch protection on every open
|
|
# PR. Keep this job non-required until it has a few weeks of green.
|
|
server-musl:
|
|
name: tty7-server musl (${{ matrix.target }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
target:
|
|
- x86_64-unknown-linux-musl
|
|
- aarch64-unknown-linux-musl
|
|
# `-C strip=symbols` is applied by rustc through the linker, so it strips the
|
|
# aarch64 output from an x86_64 runner — plain `strip(1)` would not. Set here
|
|
# rather than in [profile.release] because the release profile is shared with
|
|
# the GUI builds, which keep their symbols.
|
|
env:
|
|
RUSTFLAGS: -C strip=symbols
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
# zig supplies both the musl sysroot and the C cross-compiler, which is
|
|
# what makes one x86_64 runner able to emit both musl targets. russh's
|
|
# default crypto backend (aws-lc-rs) builds a sizable C/asm library through
|
|
# cmake, and that is the part every other approach trips over: `cross`
|
|
# needs a custom image to get cmake into the sandbox, and Ubuntu's
|
|
# `musl-tools` only ships an x86_64 `musl-gcc` wrapper with no C++ driver
|
|
# and nothing at all for aarch64. Verified locally: both targets link
|
|
# statically and the resulting binaries run under Alpine.
|
|
#
|
|
# If setup-zig ever becomes a problem (its GitHub repo is a mirror of a
|
|
# Codeberg original), cargo-zigbuild also accepts zig from PyPI —
|
|
# `pip3 install ziglang`, which it finds via CARGO_ZIGBUILD_PYTHON_PATH —
|
|
# so this can drop to zero third-party actions without changing anything
|
|
# else.
|
|
- uses: mlugg/setup-zig@v2
|
|
with:
|
|
version: 0.16.0
|
|
|
|
- uses: taiki-e/install-action@v2
|
|
with:
|
|
tool: cargo-zigbuild
|
|
|
|
- uses: Swatinem/rust-cache@v2
|
|
with:
|
|
key: ${{ matrix.target }}
|
|
|
|
# The crate split lands separately; until `tty7-server` exists as a
|
|
# workspace member this job has nothing to build. Skip cleanly rather than
|
|
# fail, so the workflow can land before the split and simply start working
|
|
# once it arrives. `--no-deps` keeps this to a manifest parse — no
|
|
# resolution, no network.
|
|
- name: Look for the tty7-server package
|
|
id: probe
|
|
run: |
|
|
set -euo pipefail
|
|
if cargo metadata --no-deps --format-version 1 \
|
|
| jq -e '[.packages[].name] | index("tty7-server")' >/dev/null; then
|
|
echo "present=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::tty7-server is not a workspace member yet (crate split, M1) — nothing to build"
|
|
fi
|
|
|
|
# `-p tty7-server` addresses the package by name, so this survives whatever
|
|
# directory layout the split settles on. It also keeps feature unification
|
|
# scoped to the server's own dependency graph: the GUI crate is the one
|
|
# that turns on tty7-core's `gssapi` feature, and that feature cannot build
|
|
# under musl (libgssapi-sys wants a system MIT/Heimdal krb5). Building the
|
|
# whole workspace here would drag it in and fail.
|
|
- name: Build static tty7-server
|
|
if: steps.probe.outputs.present == 'true'
|
|
run: cargo zigbuild --release --locked -p tty7-server --target ${{ matrix.target }}
|
|
|
|
- name: Assert the binary is static
|
|
if: steps.probe.outputs.present == 'true'
|
|
run: bash .github/scripts/assert-static.sh "target/${{ matrix.target }}/release/tty7-server"
|
|
|
|
# The same headless server for remote Macs. A second job rather than more rows
|
|
# in `server-musl`: that one exists to cross-compile from Linux with zig, and
|
|
# neither half of it applies here — an Apple target needs an Apple runner for
|
|
# the SDK, and there is no musl to link.
|
|
#
|
|
# Both slices come off one arm64 runner. Cross-compiling to x86_64 needs
|
|
# nothing but the rustup target: the Xcode SDK carries both architectures, and
|
|
# the linker is the same one.
|
|
#
|
|
# Compile-only, like `server-musl` — release.yml and nightly.yml carry the
|
|
# same job with an upload step. Non-required until it has a few weeks of
|
|
# green, for the branch-protection reason spelled out above.
|
|
server-macos:
|
|
name: tty7-server macOS (${{ matrix.target }})
|
|
runs-on: macos-latest
|
|
timeout-minutes: 45
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- target: aarch64-apple-darwin
|
|
arch: arm64
|
|
- target: x86_64-apple-darwin
|
|
arch: x86_64
|
|
env:
|
|
RUSTFLAGS: -C strip=symbols
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- uses: Swatinem/rust-cache@v2
|
|
with:
|
|
key: ${{ matrix.target }}
|
|
|
|
- name: Look for the tty7-server package
|
|
id: probe
|
|
run: |
|
|
set -euo pipefail
|
|
if cargo metadata --no-deps --format-version 1 \
|
|
| jq -e '[.packages[].name] | index("tty7-server")' >/dev/null; then
|
|
echo "present=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::tty7-server is not a workspace member yet (crate split, M1) — nothing to build"
|
|
fi
|
|
|
|
# `-p tty7-server` for the same two reasons as the musl job: it addresses
|
|
# the package by name whatever the layout, and it keeps the GUI's `gssapi`
|
|
# feature out of the unified feature set. That feature does resolve on
|
|
# macOS, unlike under musl, but linking Heimdal into a binary that gets
|
|
# pushed to someone else's Mac is exactly the kind of dependency
|
|
# assert-macho.sh is there to refuse.
|
|
- name: Build tty7-server
|
|
if: steps.probe.outputs.present == 'true'
|
|
run: cargo build --release --locked -p tty7-server --target ${{ matrix.target }}
|
|
|
|
# Ad-hoc, so it needs no identity and no secrets — this is not the code
|
|
# signing the GUI bundles get, and it buys nothing with Gatekeeper. It is
|
|
# the signature the arm64 kernel requires before it will execute anything
|
|
# at all. The linker already applies one to the arm64 slice; x86_64 comes
|
|
# out bare, and that is the slice a Rosetta shell on an Apple Silicon Mac
|
|
# asks for. `--force` makes this idempotent across both.
|
|
- name: Ad-hoc sign the binary
|
|
if: steps.probe.outputs.present == 'true'
|
|
run: codesign --force --sign - "target/${{ matrix.target }}/release/tty7-server"
|
|
|
|
- name: Assert the binary is self-contained
|
|
if: steps.probe.outputs.present == 'true'
|
|
run: |
|
|
bash .github/scripts/assert-macho.sh \
|
|
"target/${{ matrix.target }}/release/tty7-server" "${{ matrix.arch }}"
|