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
506 lines
20 KiB
YAML
506 lines
20 KiB
YAML
name: Nightly
|
|
|
|
# Unattended nightly channel: build `main` every night, stamp a pre-release
|
|
# CalVer version (<next-stable>-nightly.<YYYYMMDDHHMM>), and publish everything
|
|
# to a single rolling `nightly` prerelease. Stable releases (release.yml) are
|
|
# untouched.
|
|
#
|
|
# This is a real update channel, not just a build: an installation set to
|
|
# Nightly reads /releases/tags/nightly and rolls from one of these to the next.
|
|
# Stable reads /releases/latest, which excludes prereleases, so it never sees
|
|
# them. See `core::update::release_endpoint`.
|
|
|
|
on:
|
|
schedule:
|
|
- cron: "0 18 * * *" # 18:00 UTC = 02:00 Beijing
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
concurrency:
|
|
group: nightly
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
# Decide whether tonight needs a build, and compute the version once.
|
|
plan:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
build: ${{ steps.plan.outputs.build }}
|
|
version: ${{ steps.plan.outputs.version }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0 # full history + tags — needed for the skip check and version math
|
|
|
|
- id: plan
|
|
run: |
|
|
# Skip when HEAD is already published: last night's nightly points at
|
|
# it (main hasn't moved) or a stable tag does (tonight would just
|
|
# rebuild the release under a nightly name).
|
|
if git tag --points-at HEAD | grep -qE '^(nightly$|v[0-9])'; then
|
|
echo "nothing new since the last published build — skipping"
|
|
echo "build=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
# Nightly version = next stable patch + timestamp suffix, so semver
|
|
# ordering lands between the previous and the next stable release:
|
|
# 26.7.0 < 26.7.1-nightly.202607161800 < 26.7.1.
|
|
#
|
|
# To the minute, not to the day: two builds on one day is what
|
|
# workflow_dispatch is for, and a date alone gives them the same
|
|
# version, which the updater reads as "already up to date" and never
|
|
# offers to the people who took the morning build. Still one numeric
|
|
# identifier, which is what `parse_version` orders nightlies by — it
|
|
# compares them all, so this stays sound if a segment is ever added.
|
|
LAST=$(git tag -l 'v*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
|
|
BASE=${LAST#v}
|
|
NEXT="${BASE%.*}.$(( ${BASE##*.} + 1 ))"
|
|
VERSION="${NEXT}-nightly.$(date -u +%Y%m%d%H%M)"
|
|
echo "building $VERSION from ${GITHUB_SHA::7}"
|
|
echo "build=true" >> "$GITHUB_OUTPUT"
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
|
|
# Mirrors release.yml's build matrix; keep the two in sync when editing.
|
|
build:
|
|
# Also behind server-musl: the Windows installer embeds its Linux musl
|
|
# binary for WSL. See the same note in release.yml.
|
|
needs: [plan, server-musl]
|
|
if: needs.plan.outputs.build == 'true'
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- runner: macos-14
|
|
os: macos
|
|
arch: arm64
|
|
target: aarch64-apple-darwin
|
|
- 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
|
|
|
|
# Everything versioned — the binary (CARGO_PKG_VERSION), asset names,
|
|
# DMG plist, Inno installer — reads Cargo.toml, so stamping it is the only
|
|
# edit needed. Shared with the server-musl job below, and it fails loudly
|
|
# if the manifest's shape ever moves the version line out from under it.
|
|
- name: Stamp nightly version
|
|
working-directory: tty7
|
|
shell: bash
|
|
run: bash .github/scripts/stamp-version.sh "${{ needs.plan.outputs.version }}"
|
|
|
|
- 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
|
|
|
|
# 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 --target "${{ matrix.target }}"
|
|
cargo build --release --features updater \
|
|
--bin tty7-updater --target "${{ matrix.target }}"
|
|
|
|
- name: Bundle macOS DMG
|
|
if: matrix.os == 'macos'
|
|
working-directory: tty7
|
|
env:
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
|
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: Verify macOS Nightly update archive
|
|
if: matrix.os == 'macos'
|
|
working-directory: tty7
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="${{ needs.plan.outputs.version }}"
|
|
ZIP="dist/tty7-${VERSION}-macos-${{ matrix.arch }}.zip"
|
|
VERIFY_ROOT="$RUNNER_TEMP/tty7-nightly-update-verify"
|
|
rm -rf "$VERIFY_ROOT"
|
|
mkdir -p "$VERIFY_ROOT"
|
|
/usr/bin/ditto -x -k "$ZIP" "$VERIFY_ROOT"
|
|
APP="$VERIFY_ROOT/tty7.app"
|
|
test -x "$APP/Contents/MacOS/tty7-updater"
|
|
ACTUAL_VERSION="$(/usr/libexec/PlistBuddy \
|
|
-c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist")"
|
|
test "$ACTUAL_VERSION" = "$VERSION"
|
|
/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP"
|
|
|
|
- name: Package Linux tarball
|
|
if: matrix.os == 'linux'
|
|
working-directory: tty7
|
|
run: bash .github/scripts/bundle-linux.sh "${{ matrix.target }}" "${{ matrix.arch }}"
|
|
|
|
- name: Package Linux AppImage
|
|
if: matrix.os == 'linux'
|
|
working-directory: tty7
|
|
run: bash .github/scripts/bundle-appimage.sh "${{ matrix.target }}" "${{ matrix.arch }}"
|
|
|
|
# The same facts the in-app updater checks on the user's machine,
|
|
# checked here so a packaging mistake fails the nightly instead. See
|
|
# the twin step in release.yml.
|
|
- name: Verify Linux AppImage update package
|
|
if: matrix.os == 'linux'
|
|
working-directory: tty7
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="${{ needs.plan.outputs.version }}"
|
|
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"
|
|
|
|
# See release.yml for why this is best-effort rather than required.
|
|
- name: Fetch the bundled Linux server
|
|
if: matrix.os == 'windows'
|
|
continue-on-error: true
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: nightly-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 }}"'
|
|
|
|
# Nightly builds the same packages as release.yml, so it gets the same
|
|
# check. Nightly is not an update channel — nobody updates *into* these
|
|
# artifacts — but a marker or version regression shows up here a night
|
|
# before it would reach a stable release.
|
|
- name: Verify Windows update package
|
|
if: matrix.os == 'windows'
|
|
working-directory: tty7
|
|
shell: pwsh
|
|
run: >-
|
|
& ./.github/scripts/verify-windows-package.ps1
|
|
"${{ matrix.arch }}" "${{ needs.plan.outputs.version }}"
|
|
|
|
# Same glob list as release.yml's Release step: 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: nightly-${{ 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
|
|
|
|
# Mirrors release.yml's server-musl job; keep the two in sync when editing.
|
|
# Nightly carries the server binaries too so the remote-install path can
|
|
# be exercised against the rolling channel instead of waiting for a tag.
|
|
server-musl:
|
|
needs: plan
|
|
if: needs.plan.outputs.build == 'true'
|
|
strategy:
|
|
fail-fast: false
|
|
# `target` is the build triple; `asset` is the published filename. Kept
|
|
# apart for the reason release.yml spells out: the triple's vendor field is
|
|
# `unknown`, and a download name has no business carrying it.
|
|
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
|
|
|
|
# Stamped for the same reason the GUI builds are: the server reports
|
|
# CARGO_PKG_VERSION over the wire during the version handshake, and a
|
|
# nightly server claiming the last stable version would make that
|
|
# negotiation lie. The asset *name* is version-free either way.
|
|
- name: Stamp nightly version
|
|
working-directory: tty7
|
|
run: bash .github/scripts/stamp-version.sh "${{ needs.plan.outputs.version }}"
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- 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 }}
|
|
|
|
- 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 — tonight's nightly carries no remote-server assets"
|
|
fi
|
|
|
|
# No `--locked` here, matching the rest of nightly: the version stamp above
|
|
# rewrites Cargo.toml, and cargo has to be free to refresh the root
|
|
# package's own lock entry.
|
|
- name: Build static tty7-server
|
|
if: steps.probe.outputs.present == 'true'
|
|
working-directory: tty7
|
|
run: cargo zigbuild --release -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"
|
|
|
|
- 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: nightly-${{ matrix.asset }}
|
|
path: tty7/dist/${{ matrix.asset }}
|
|
if-no-files-found: error
|
|
|
|
# Mirrors release.yml's server-macos job; keep the two in sync when editing.
|
|
# Nightly carries the macOS servers for the same reason it carries the Linux
|
|
# ones: so the remote-install path onto a Mac can be exercised against the
|
|
# rolling channel instead of waiting for a tag.
|
|
server-macos:
|
|
needs: plan
|
|
if: needs.plan.outputs.build == 'true'
|
|
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
|
|
|
|
# Stamped for the same reason the musl servers are: the version the server
|
|
# reports during the handshake has to be tonight's, not the last stable.
|
|
- name: Stamp nightly version
|
|
working-directory: tty7
|
|
run: bash .github/scripts/stamp-version.sh "${{ needs.plan.outputs.version }}"
|
|
|
|
- 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 — tonight's nightly carries no macOS remote-server assets"
|
|
fi
|
|
|
|
# No `--locked`, matching the rest of nightly: the version stamp above
|
|
# rewrites Cargo.toml and cargo has to be free to refresh its lock entry.
|
|
- name: Build tty7-server
|
|
if: steps.probe.outputs.present == 'true'
|
|
working-directory: tty7
|
|
run: cargo build --release -p tty7-server --target ${{ matrix.target }}
|
|
|
|
# Ad-hoc, for the reason release.yml spells out: it is what arm64 requires
|
|
# before it will execute anything, the linker only applies it to the arm64
|
|
# slice, and x86_64 is what a Rosetta shell asks for.
|
|
- 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: nightly-${{ matrix.asset }}
|
|
path: tty7/dist/${{ matrix.asset }}
|
|
if-no-files-found: error
|
|
|
|
# Single publish step after all platforms succeed, so the rolling release is
|
|
# always complete — a failed platform means tonight's nightly is skipped
|
|
# entirely and users keep yesterday's, never a partial asset set.
|
|
publish:
|
|
needs: [plan, build, server-musl, server-macos]
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
VERSION: ${{ needs.plan.outputs.version }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: dist
|
|
merge-multiple: true
|
|
|
|
# The version has to be readable by a machine, and the tag cannot carry
|
|
# it: `nightly` is force-moved to a new commit every night, so the release
|
|
# object's `tag_name` is the literal string "nightly". Without this the
|
|
# updater is left reverse-engineering the version out of asset filenames.
|
|
#
|
|
# Written before checksums.txt so the manifest is hashed along with
|
|
# everything else. The updater reads it straight from the release without
|
|
# checking that hash — it only decides which version is on offer, and the
|
|
# package it then selects is verified the usual way — but the entry has to
|
|
# be there for anyone auditing the release by hand.
|
|
- name: Generate nightly.json
|
|
run: |
|
|
set -euo pipefail
|
|
jq -n \
|
|
--arg version "$VERSION" \
|
|
--arg commit "$GITHUB_SHA" \
|
|
--arg published_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
'{version: $version, commit: $commit, published_at: $published_at}' \
|
|
> dist/nightly.json
|
|
cat dist/nightly.json
|
|
|
|
# Same contract as release.yml — see `install::asset`. Written
|
|
# into dist/ before the upload below so it ships as an asset like any
|
|
# other, and so the prune step at the end sees it as current.
|
|
- name: Generate checksums.txt
|
|
run: |
|
|
set -euo pipefail
|
|
cd dist
|
|
rm -f checksums.txt
|
|
# Built in $RUNNER_TEMP and moved in: a redirect straight into dist/
|
|
# creates the file before find walks the directory, so it would hash
|
|
# itself as a zero-byte entry. `xargs -r` so an empty dist/ fails
|
|
# instead of hanging on stdin.
|
|
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
|
|
|
|
- name: Update rolling nightly release
|
|
run: |
|
|
# Move the tag first so the release object follows it to this SHA.
|
|
git push -f origin "$GITHUB_SHA:refs/tags/nightly"
|
|
|
|
TITLE="Nightly $VERSION"
|
|
NOTES="Automated nightly build of \`main\` @ ${GITHUB_SHA::7} ($(date -u +%F)). Rolling prerelease — assets are replaced every night; for the latest stable release see https://github.com/${GITHUB_REPOSITORY}/releases/latest."
|
|
|
|
if gh release view nightly >/dev/null 2>&1; then
|
|
gh release edit nightly --prerelease --title "$TITLE" --notes "$NOTES"
|
|
else
|
|
gh release create nightly --prerelease --title "$TITLE" --notes "$NOTES"
|
|
fi
|
|
|
|
# Upload before deleting: date-suffixed names never collide across
|
|
# nights, so both sets briefly coexist — if an upload dies midway,
|
|
# yesterday's complete nightly is still intact.
|
|
gh release upload nightly dist/* --clobber
|
|
|
|
# Now prune the previous night's assets (anything we didn't upload).
|
|
gh release view nightly --json assets -q '.assets[].name' |
|
|
while read -r name; do
|
|
[ -e "dist/$name" ] || gh release delete-asset nightly "$name" -y
|
|
done
|