Files
tty7/.github/workflows/release.yml
T
l0ng-ai 3c36632cbe fix(release): assemble the release as a draft once every platform is green
The four platform jobs each ran softprops/action-gh-release, so the first
one to finish published a release carrying only its own assets. That
release immediately became /releases/latest, which the in-app update check
polls — users were prompted to download a version whose assets were still
being built, and macOS users in particular could open the page minutes
before a .dmg existed. A permanently failed platform left the gap forever.

Build jobs now hand their bundles to a single draft-release job via
upload-artifact. It runs only after all four succeed, and assembles a
draft: drafts are invisible to /releases/latest, so nothing is advertised
until the release skill has verified the six assets, written the notes,
and published it by hand.

This mirrors the shape nightly.yml already used.
2026-07-25 16:33:37 +08:00

174 lines
7.3 KiB
YAML

name: Release
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
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: |
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
# `--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.
- name: Build
working-directory: tty7
run: cargo build --release --locked --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 }}"
- name: Package Windows installer + zip
if: matrix.os == 'windows'
working-directory: tty7
shell: pwsh
run: '& ./.github/scripts/bundle-windows.ps1 "${{ matrix.target }}" "${{ 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
# Single assembly step, after all four platforms succeed. 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 six assets and
# writes the body first, then flips the draft. See .claude/skills/release/SKILL.md.
draft-release:
needs: build
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
# 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"