mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
`tty7-server-x86_64-unknown-linux-musl` was never a name anyone chose. Both
workflows staged the file as `tty7-server-${{ matrix.target }}`, so the build
triple went straight into a published filename — and the triple's *vendor*
field, for a Linux target with no particular vendor, is the literal word
`unknown`. It has been sitting on the releases page reading like a failed
lookup.
Of the triple's four fields only two say anything to whoever downloads this:
the architecture, which is what `asset_for_uname` picks by, and `musl`, which
is why one file runs on any distribution. So:
tty7-server-x86_64-unknown-linux-musl → tty7-server-linux-x86_64-musl
tty7-server-aarch64-unknown-linux-musl → tty7-server-linux-aarch64-musl
`<os>-<arch>` in that order because that is what the GUI assets in the same
release already use (`tty7-<version>-linux-x86_64.tar.gz`). One release should
be one naming scheme; it was two.
The triple stays everywhere it really is a build target — `cargo zigbuild
--target`, the `target/<triple>/release` path, the rust-cache key, ci.yml's
matrix. The workflows now carry both: `target` for the build, `asset` for the
filename, deliberately not the same string.
This name is a contract with more than the release step, and all of it moves
together:
- `install::asset::{ASSET_X86_64, ASSET_AARCH64}`, which is what the client
appends to a release URL.
- `bundle-windows.ps1`, which stages the musl binary for WSL. `wsl.rs` looks
for `<dir>/<asset name>` with nothing translating, so the *filename* is as
much a contract as the `server/` directory is — now said out loud in both
places, along with the consequence for `TTY7_BUNDLED_SERVER_DIR`: a
cross-compile has to be copied to the asset name, not left as `tty7-server`.
- The GUI's install prompt fixture, the checksum manifest fixtures, and the
`MissingBundled` assertions.
Nothing globs the old shape: `gh release upload dist/*`, `checksums.txt`'s
`find`, and the installer's `server\*` are all name-agnostic.
A new test pins both names as literals — the module header already says this
naming is "a *literal* contract with the release workflow", and asserting the
consts against themselves asserted nothing. It also fails on the substring
`unknown`, since that word only ever arrived here by way of `matrix.target`,
and checks neither name contains the other, which is what
`checksums::expected_digest` says out loud that it relies on.
Compatibility: a stable client asks its own frozen tag, which keeps whichever
name it shipped with, so every released client keeps working. The rolling
`nightly` tag is replaced each night and its prune step drops assets the run
did not upload — so an *already installed* nightly client 404s on the server
download until it updates itself. Accepted deliberately; the next release is
what has to be right.
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
323 lines
12 KiB
YAML
323 lines
12 KiB
YAML
name: Nightly
|
|
|
|
# Unattended nightly channel: build `main` every night, stamp a pre-release
|
|
# CalVer version (<next-stable>-nightly.<YYYYMMDD>), and publish everything to
|
|
# a single rolling `nightly` prerelease. Stable releases (release.yml) are
|
|
# untouched; the update checker ignores prereleases via /releases/latest.
|
|
|
|
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 + date suffix, so semver
|
|
# ordering lands between the previous and the next stable release:
|
|
# 26.7.0 < 26.7.1-nightly.20260716 < 26.7.1.
|
|
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)"
|
|
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: |
|
|
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
|
|
|
|
- name: Build
|
|
working-directory: tty7
|
|
run: cargo build --release --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: 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 }}"
|
|
|
|
# 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@v7
|
|
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 }}"'
|
|
|
|
# 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
|
|
|
|
# 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]
|
|
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
|
|
|
|
# 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
|