Files
tty7/.github/workflows/nightly.yml
T
l0ng-ai 791d0d0cfa Merge remote-tracking branch 'origin/main' into polish/ralph-wc
# 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
2026-08-22 16:48:33 +08:00

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