mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 16:02:24 +00:00
# Conflicts: # README.md # README.zh-CN.md # crates/tty7-cli/src/cli.rs # crates/tty7-cli/src/server.rs # crates/tty7-core/src/core/config.rs # crates/tty7-core/src/core/git/status.rs # crates/tty7-core/src/daemon/install/wsl.rs # crates/tty7-core/src/daemon/protocol.rs # crates/tty7-core/src/daemon/spawn.rs # crates/tty7-core/src/daemon/ssh/mod.rs # src/terminal/completion.rs # src/terminal/remote.rs # src/ui/app.rs # src/ui/i18n/en.rs # src/ui/i18n/ja.rs # src/ui/i18n/zh.rs # src/ui/tree_sync.rs
453 lines
20 KiB
YAML
453 lines
20 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: |
|
|
bash .github/scripts/install-linux-deps.sh
|
|
# Packaging only, and nothing to do with building tty7: libfuse2 is
|
|
# what an AppImage needs to run at all, `file` and imagemagick are
|
|
# used while assembling one. Kept separate from the build list so
|
|
# neither set is read as the reason for the other.
|
|
sudo apt-get install -y libfuse2 file imagemagick
|
|
|
|
- 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.
|
|
# The updater ships on every GUI platform now: inside the macOS bundle,
|
|
# beside the Windows app, and inside the Linux AppImage.
|
|
- name: Build
|
|
working-directory: tty7
|
|
shell: bash
|
|
run: |
|
|
cargo build --release --locked --target "${{ matrix.target }}"
|
|
cargo build --release --locked --features updater \
|
|
--bin tty7-updater --target "${{ matrix.target }}"
|
|
|
|
# ---- 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 in-app updater refuses an image whose bundled 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 Linux AppImage update package
|
|
if: matrix.os == 'linux'
|
|
working-directory: tty7
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="$(grep -m1 '^version = "' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
|
|
IMAGE="$PWD/dist/tty7-${VERSION}-linux-${{ matrix.arch }}.AppImage"
|
|
VERIFY_ROOT="$RUNNER_TEMP/tty7-appimage-update-verify"
|
|
rm -rf "$VERIFY_ROOT"
|
|
mkdir -p "$VERIFY_ROOT"
|
|
(cd "$VERIFY_ROOT" && "$IMAGE" --appimage-extract >/dev/null)
|
|
test -x "$VERIFY_ROOT/squashfs-root/usr/bin/tty7-app"
|
|
test -x "$VERIFY_ROOT/squashfs-root/usr/bin/tty7-updater"
|
|
grep -Fxq "X-AppImage-Version=${VERSION}" \
|
|
"$VERIFY_ROOT/squashfs-root/usr/share/applications/tty7.desktop"
|
|
|
|
# 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"
|