mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-28 08:02:39 +00:00
`closing_the_pool_drops_queued_work` never tested a queued job. `submit`
grows the pool whenever `jobs.len() > idle`, so blocking one worker and
submitting a second job spawns a second worker and runs it immediately —
the assertion only held when `close()` beat a brand-new thread to the state
lock. On a loaded machine it lost that race about one run in six.
A job only stays queued when the pool is saturated, so the test now fills it
to MAX_WORKERS first, asserts the job is on the queue before closing, and
waits for the workers to drain afterwards. 14 consecutive `-p tty7-core --lib`
runs green, against 2 failures in 16 before.
Placeholders in a translation were checked by nobody: `apply_template`
substitutes by name and leaves anything it was not given alone, so a `{name}`
dropped from the zh string reaches the user as a sentence that has simply
stopped naming the host, and an invented one draws braces on screen. Neither
is a missing or empty translation, which is all the parity test could see.
Both are now checked, for the plural and select branches too. Nothing was
wrong today.
The `KEPT_IN_ENGLISH` staleness check printed rather than asserted, and cargo
swallows a passing test's stdout, so the list could only ever grow.
The Linux build dependencies existed in four copies across three workflows;
release and nightly had the AppImage packaging tools (libfuse2, file,
imagemagick) mixed into the same list, which reads as if they were needed to
build. One script now, with the packaging extras installed separately, and the
ci.yml comment points at the docs page that actually documents the list rather
than at the README, which never did.
2931 tests pass.
486 lines
19 KiB
YAML
486 lines
19 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
|
|
|
|
- name: Build
|
|
working-directory: tty7
|
|
shell: bash
|
|
run: |
|
|
cargo build --release --target "${{ matrix.target }}"
|
|
if [[ "${{ matrix.os }}" == "macos" || "${{ matrix.os }}" == "windows" ]]; then
|
|
cargo build --release --features updater \
|
|
--bin tty7-updater --target "${{ matrix.target }}"
|
|
fi
|
|
|
|
- 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 }}"
|
|
|
|
# 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
|