mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
* feat(updater): add windows online updates
* feat(updater): support online updates for windows portable zip builds
f
* feat(updater): support online updates for nightly build
* fix(updater): strengthen post-download update verification
* feat(updater): support explicit stable and nightly channel switching
* fix(i18n): localize update settings ui
* fix(settings): prevent slider value labels from wrapping
* feat(updater): drop the nightly channel, refuse all-users Windows installs
Follow-up to the Windows updater work on this branch, applying maintainer
review.
Nightly is a build channel, not an update channel. The updater consults
`/releases/latest` again and nothing else, so it behaves on Windows exactly
as it already does on macOS: a Nightly build is offered the stable release
that supersedes it and graduates out of the prerelease, and no rolling
prerelease can become a source of code that gets executed on a user's
machine. Removed with it: the `UpdateChannel` enum and its version-string
inference, the `tags/nightly` query, the cross-channel version-ordering
bypass, the Settings → About channel row, the rolling-tag
`update-manifest.json` and the i18n keys that only served them.
`parse_version` and `is_update_available` are byte-identical to main again.
Nightly builds are untouched, and still carry tty7-updater plus the macOS
update archive — a Nightly user needs a working helper to reach the stable
release that replaces their build.
An all-users Windows installation is no longer updated in place. Running the
release Setup silently as the signed-in user cannot replace
`C:\Program Files\tty7`: Inno resolves `{autopf}` to `%LocalAppData%\Programs`
and installs a second copy beside the real one, or re-launches itself
elevated and puts a bare UAC prompt for an unsigned executable in `%TEMP%` in
front of a user whose GUI just vanished. tty7 declines both and points at the
release page. Detection reads Inno's own `HKLM` state for the frozen AppId and
independently probes whether the directory accepts writes, so a relocated or
pruned installation is caught too; the decision is a pure function with unit
tests, and it is re-checked before the download as well as during it.
Release and Nightly now verify the Windows packages they just built, mirroring
the macOS update-archive step: the install marker, tty7-updater.exe, the ZIP
layout the updater will accept and the PE versions it will demand. Every fact
the updater checks on the user's machine after downloading is checked here
instead, so a packaging mistake fails the build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
242 lines
11 KiB
YAML
242 lines
11 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"
|