mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 16: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>
433 lines
19 KiB
YAML
433 lines
19 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
build:
|
|
# The Windows installer embeds the Linux musl `tty7-server` so a WSL distro
|
|
# can be served the binary the client already shipped with, instead of
|
|
# downloading one (WSL installs nothing over the network). That
|
|
# binary comes from `server-musl`, so the two jobs can no longer run in
|
|
# parallel. Serialising all four platforms behind it costs a few minutes on
|
|
# a release — cheap next to splitting the Windows entry into its own job and
|
|
# duplicating the whole toolchain/caching preamble.
|
|
needs: server-musl
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- runner: macos-14
|
|
os: macos
|
|
arch: arm64
|
|
target: aarch64-apple-darwin
|
|
# macos-13 was retired; macos-15-intel is the remaining hosted x86_64 image.
|
|
- runner: macos-15-intel
|
|
os: macos
|
|
arch: x86_64
|
|
target: x86_64-apple-darwin
|
|
- runner: windows-latest
|
|
os: windows
|
|
arch: x86_64
|
|
target: x86_64-pc-windows-msvc
|
|
- runner: ubuntu-latest
|
|
os: linux
|
|
arch: x86_64
|
|
target: x86_64-unknown-linux-gnu
|
|
runs-on: ${{ matrix.runner }}
|
|
steps:
|
|
- name: Checkout tty7
|
|
uses: actions/checkout@v4
|
|
with:
|
|
path: tty7
|
|
|
|
# gpui-component is pulled as a git dependency (see Cargo.toml's patch
|
|
# section), so no sibling checkout is needed.
|
|
|
|
# gpui's Linux backends resolve the x11/wayland/xkb/font dev packages via
|
|
# pkg-config at build time — the same set the README documents for
|
|
# building from source on Linux.
|
|
- name: Install Linux system dependencies
|
|
if: matrix.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 libfuse2 file imagemagick
|
|
echo "LIBGSSAPI_IMPL=mit" >> "$GITHUB_ENV"
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: tty7
|
|
|
|
# `--locked` because a release must ship the dependency set the tag
|
|
# recorded, not whatever cargo would re-resolve at build time. Safe here
|
|
# (unlike nightly) precisely because nothing rewrites Cargo.toml: this is
|
|
# a plain checkout of the tagged commit.
|
|
- name: Build
|
|
working-directory: tty7
|
|
shell: bash
|
|
run: |
|
|
cargo build --release --locked --target "${{ matrix.target }}"
|
|
if [[ "${{ matrix.os }}" == "macos" || "${{ matrix.os }}" == "windows" ]]; then
|
|
cargo build --release --locked --features updater \
|
|
--bin tty7-updater --target "${{ matrix.target }}"
|
|
fi
|
|
|
|
# ---- Packaging: one step per OS ----------------------------------------
|
|
# macOS gets a signed + notarized drag-to-Applications DMG. Windows gets
|
|
# an Inno Setup installer plus a portable zip; Linux a tarball — both
|
|
# unsigned, of the self-contained binary (fonts are embedded via
|
|
# include_bytes!; the Windows icon is compiled in via build.rs).
|
|
- name: Bundle macOS DMG
|
|
if: matrix.os == 'macos'
|
|
working-directory: tty7
|
|
env:
|
|
# macOS code signing — the cert is imported into a throwaway keychain.
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
|
# Notarization — required for Developer ID builds to pass Gatekeeper.
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: bash .github/scripts/bundle-macos.sh "${{ matrix.target }}" "${{ matrix.arch }}"
|
|
|
|
- name: Package Linux tarball
|
|
if: matrix.os == 'linux'
|
|
working-directory: tty7
|
|
run: bash .github/scripts/bundle-linux.sh "${{ matrix.target }}" "${{ matrix.arch }}"
|
|
|
|
# AppImage bundles the x11/wayland/xkb/font libs so it runs on Fedora/Arch/
|
|
# etc., not just Ubuntu. Kept separate from the tarball step so the tarball
|
|
# still ships even if AppImage tooling changes upstream.
|
|
- name: Package Linux AppImage
|
|
if: matrix.os == 'linux'
|
|
working-directory: tty7
|
|
run: bash .github/scripts/bundle-appimage.sh "${{ matrix.target }}" "${{ matrix.arch }}"
|
|
|
|
# The bundled server for WSL. `continue-on-error` mirrors `server-musl`'s
|
|
# own probe step: if there is no server asset, the release still ships and
|
|
# `bundle-windows.ps1` warns. It is not silent at runtime either — a WSL
|
|
# connect then fails with `MissingBundled`, naming every directory it
|
|
# searched, rather than quietly falling back to a download.
|
|
- name: Fetch the bundled Linux server
|
|
if: matrix.os == 'windows'
|
|
continue-on-error: true
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: release-tty7-server-linux-x86_64-musl
|
|
path: tty7/bundled-server
|
|
|
|
- name: Package Windows installer + zip
|
|
if: matrix.os == 'windows'
|
|
working-directory: tty7
|
|
shell: pwsh
|
|
run: '& ./.github/scripts/bundle-windows.ps1 "${{ matrix.target }}" "${{ matrix.arch }}"'
|
|
|
|
# The in-app updater refuses a package whose marker, helper or stamped
|
|
# version is wrong — on the user's machine, after the download. Check the
|
|
# same facts here so a packaging mistake fails the release instead.
|
|
- name: Verify Windows update package
|
|
if: matrix.os == 'windows'
|
|
working-directory: tty7
|
|
shell: pwsh
|
|
run: '& ./.github/scripts/verify-windows-package.ps1 "${{ matrix.arch }}"'
|
|
|
|
# Hand the artifacts to the assemble job rather than uploading them to the
|
|
# release here. Four parallel jobs each publishing their own slice would
|
|
# make the release "latest" the moment the *first* platform finished — the
|
|
# in-app update check (src/core/update.rs) reads /releases/latest, so users
|
|
# would be prompted to download a release that was still missing most of
|
|
# its assets. Same glob list as before: the bundle scripts leave
|
|
# intermediates in dist/ (tty7.app, entitlements.plist, the Windows staging
|
|
# dir) that must not reach the release assets.
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-${{ matrix.os }}-${{ matrix.arch }}
|
|
path: |
|
|
tty7/dist/*.dmg
|
|
tty7/dist/*.tar.gz
|
|
tty7/dist/*.zip
|
|
tty7/dist/*-setup.exe
|
|
tty7/dist/*.AppImage
|
|
if-no-files-found: error
|
|
|
|
# The headless server binary remote workspaces install on the far machine
|
|
# (decision D10). Statically linked against musl so a single binary runs
|
|
# on any distro whatever its glibc vintage, and shipped as a bare executable
|
|
# rather than an archive so the client can fetch exactly one file and verify it
|
|
# against checksums.txt. The asset names are a contract with the installer:
|
|
# `tty7_core::daemon::install::asset` derives them from `uname -sm`.
|
|
#
|
|
# Separate from the `build` matrix above because it shares nothing with it: no
|
|
# GUI toolchain, no bundling, no code signing, two targets off one runner.
|
|
server-musl:
|
|
strategy:
|
|
fail-fast: false
|
|
# `target` is the build triple; `asset` is the published filename. They
|
|
# are deliberately not the same string — the triple's vendor field is
|
|
# `unknown`, which says nothing to anyone reading the releases page. See
|
|
# `install::asset::ASSET_X86_64`, which pins these two names as literals.
|
|
matrix:
|
|
include:
|
|
- target: x86_64-unknown-linux-musl
|
|
asset: tty7-server-linux-x86_64-musl
|
|
- target: aarch64-unknown-linux-musl
|
|
asset: tty7-server-linux-aarch64-musl
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
RUSTFLAGS: -C strip=symbols
|
|
steps:
|
|
- name: Checkout tty7
|
|
uses: actions/checkout@v4
|
|
with:
|
|
path: tty7
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
# zig provides the musl sysroot and the C cross-compiler for both targets
|
|
# from one x86_64 runner — see the same job in ci.yml for why the
|
|
# alternatives (cross, musl-tools) do not cope with aws-lc-rs' cmake build.
|
|
- 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:
|
|
workspaces: tty7
|
|
key: ${{ matrix.target }}
|
|
|
|
# Until the crate split lands there is no tty7-server to build. Skip
|
|
# rather than fail, so this workflow can ship ahead of the split; the
|
|
# release simply carries no server assets until it arrives.
|
|
- name: Look for the tty7-server package
|
|
id: probe
|
|
working-directory: tty7
|
|
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 "::warning::tty7-server is not a workspace member yet — this release will carry no remote-server assets"
|
|
fi
|
|
|
|
# `--locked` for the same reason the GUI build uses it: a release ships the
|
|
# dependency set the tag recorded. `-p tty7-server` both addresses the
|
|
# package independently of its path and keeps feature unification off the
|
|
# GUI's `gssapi` feature, which cannot build under musl.
|
|
- name: Build static tty7-server
|
|
if: steps.probe.outputs.present == 'true'
|
|
working-directory: tty7
|
|
run: cargo zigbuild --release --locked -p tty7-server --target ${{ matrix.target }}
|
|
|
|
- name: Assert the binary is static
|
|
if: steps.probe.outputs.present == 'true'
|
|
working-directory: tty7
|
|
run: bash .github/scripts/assert-static.sh "target/${{ matrix.target }}/release/tty7-server"
|
|
|
|
# Flat, version-free asset name — the tag in the download URL carries the
|
|
# version. See `install::asset` for the contract the client
|
|
# installer derives this name from.
|
|
- name: Stage the asset
|
|
if: steps.probe.outputs.present == 'true'
|
|
working-directory: tty7
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p dist
|
|
cp "target/${{ matrix.target }}/release/tty7-server" \
|
|
"dist/${{ matrix.asset }}"
|
|
chmod +x "dist/${{ matrix.asset }}"
|
|
|
|
- uses: actions/upload-artifact@v7
|
|
if: steps.probe.outputs.present == 'true'
|
|
with:
|
|
name: release-${{ matrix.asset }}
|
|
path: tty7/dist/${{ matrix.asset }}
|
|
if-no-files-found: error
|
|
|
|
# The same server for remote Macs, published under the same contract: flat,
|
|
# version-free names that `install::asset` derives from `uname -sm`. Separate
|
|
# from `server-musl` because an Apple target needs an Apple runner, and both
|
|
# slices come off one arm64 runner — the Xcode SDK carries both. See ci.yml's
|
|
# `server-macos` for the rest of the reasoning; keep the three in sync.
|
|
#
|
|
# Not code-signed or notarized, and deliberately: this binary is never
|
|
# downloaded by the destination Mac. 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 at all. The ad-hoc signature the linker applies
|
|
# is all arm64 asks for, and `assert-macho.sh` checks it is there.
|
|
server-macos:
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- target: aarch64-apple-darwin
|
|
arch: arm64
|
|
asset: tty7-server-macos-aarch64
|
|
- target: x86_64-apple-darwin
|
|
arch: x86_64
|
|
asset: tty7-server-macos-x86_64
|
|
runs-on: macos-latest
|
|
env:
|
|
RUSTFLAGS: -C strip=symbols
|
|
steps:
|
|
- name: Checkout tty7
|
|
uses: actions/checkout@v4
|
|
with:
|
|
path: tty7
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: tty7
|
|
key: ${{ matrix.target }}
|
|
|
|
- name: Look for the tty7-server package
|
|
id: probe
|
|
working-directory: tty7
|
|
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 "::warning::tty7-server is not a workspace member yet — this release will carry no macOS remote-server assets"
|
|
fi
|
|
|
|
- name: Build tty7-server
|
|
if: steps.probe.outputs.present == 'true'
|
|
working-directory: tty7
|
|
run: cargo build --release --locked -p tty7-server --target ${{ matrix.target }}
|
|
|
|
# Ad-hoc — no identity, no secrets, and nothing to do with the notarized
|
|
# signing the GUI bundles get. It is what the arm64 kernel requires before
|
|
# it will execute a binary at all: the linker signs the arm64 slice
|
|
# itself, x86_64 comes out bare, and x86_64 is what a Rosetta shell on an
|
|
# Apple Silicon Mac asks for. `--force` makes it idempotent across both.
|
|
- name: Ad-hoc sign the binary
|
|
if: steps.probe.outputs.present == 'true'
|
|
working-directory: tty7
|
|
run: codesign --force --sign - "target/${{ matrix.target }}/release/tty7-server"
|
|
|
|
- name: Assert the binary is self-contained
|
|
if: steps.probe.outputs.present == 'true'
|
|
working-directory: tty7
|
|
run: |
|
|
bash .github/scripts/assert-macho.sh \
|
|
"target/${{ matrix.target }}/release/tty7-server" "${{ matrix.arch }}"
|
|
|
|
- name: Stage the asset
|
|
if: steps.probe.outputs.present == 'true'
|
|
working-directory: tty7
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p dist
|
|
cp "target/${{ matrix.target }}/release/tty7-server" \
|
|
"dist/${{ matrix.asset }}"
|
|
chmod +x "dist/${{ matrix.asset }}"
|
|
|
|
- uses: actions/upload-artifact@v7
|
|
if: steps.probe.outputs.present == 'true'
|
|
with:
|
|
name: release-${{ matrix.asset }}
|
|
path: tty7/dist/${{ matrix.asset }}
|
|
if-no-files-found: error
|
|
|
|
# Single assembly step, after every GUI platform and every server slice
|
|
# succeeds. The release object is
|
|
# created as a **draft** and left that way: a draft is invisible to both
|
|
# /releases/latest and the releases page, so nothing can prompt a user to
|
|
# download a version whose asset set is incomplete or whose notes are still
|
|
# empty. Publishing is the release skill's job — it verifies the platform assets and
|
|
# writes the body first, then flips the draft. See .claude/skills/release/SKILL.md.
|
|
draft-release:
|
|
needs: [build, server-musl, server-macos]
|
|
if: startsWith(github.ref, 'refs/tags/')
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
steps:
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: dist
|
|
merge-multiple: true
|
|
|
|
# sha256 over every asset, so the remote-server installer can verify what
|
|
# it downloaded before writing it to someone else's machine (a mismatch
|
|
# aborts the install outright). Generated here rather than in
|
|
# the build jobs because only this job sees the complete asset set, and a
|
|
# per-job fragment would have to be concatenated in a deterministic order
|
|
# anyway. GNU coreutils format ("<hex> <name>"), bare filenames, sorted —
|
|
# see `install::checksums` for the format the client parses.
|
|
- name: Generate checksums.txt
|
|
run: |
|
|
set -euo pipefail
|
|
cd dist
|
|
rm -f checksums.txt
|
|
# `find -type f` rather than a glob: nested files (should any appear)
|
|
# would otherwise be silently skipped, leaving an asset unverifiable.
|
|
#
|
|
# Built in $RUNNER_TEMP and moved in, rather than redirected straight
|
|
# into dist/: the `>` redirect creates its target *before* find walks
|
|
# the directory, so a file written in place would end up hashing
|
|
# itself as a zero-byte entry — a line that can never verify.
|
|
#
|
|
# `xargs -r` — without it an empty dist/ would leave sha256sum reading
|
|
# stdin and the job would hang rather than fail.
|
|
find . -type f -printf '%P\n' \
|
|
| LC_ALL=C sort | xargs -r sha256sum > "$RUNNER_TEMP/checksums.txt"
|
|
[ -s "$RUNNER_TEMP/checksums.txt" ] || { echo "::error::no assets to checksum"; exit 1; }
|
|
mv "$RUNNER_TEMP/checksums.txt" checksums.txt
|
|
sha256sum -c checksums.txt
|
|
cat checksums.txt
|
|
|
|
# Reuse an existing release rather than failing: re-triggering a tag
|
|
# (force-push after a fixed platform) must top up the same draft. If the
|
|
# release was already published, --clobber just replaces its assets and it
|
|
# stays published.
|
|
#
|
|
# Existence is probed with `release list`, not `release view`: GitHub's
|
|
# get-release-by-tag endpoint does not return drafts, so a view-based check
|
|
# could miss the very draft a previous run left behind and create a second
|
|
# one (GitHub happily allows duplicate drafts on one tag).
|
|
- name: Assemble the draft release
|
|
run: |
|
|
set -euo pipefail
|
|
# Captured into a variable, not piped into `grep -q`: -q exits on the
|
|
# first match, and the resulting SIGPIPE would make `pipefail` report
|
|
# the pipeline as failed — i.e. "found" would read as "not found".
|
|
# `release list` includes drafts (cf. its --exclude-drafts flag).
|
|
EXISTING=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 100 \
|
|
--json tagName -q '.[].tagName')
|
|
if grep -Fxq "$GITHUB_REF_NAME" <<<"$EXISTING"; then
|
|
echo "release $GITHUB_REF_NAME already exists; reusing it"
|
|
else
|
|
gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" \
|
|
--draft --title "$GITHUB_REF_NAME" --notes ""
|
|
fi
|
|
gh release upload "$GITHUB_REF_NAME" dist/* --clobber --repo "$GITHUB_REPOSITORY"
|