chore(release): v3.1.4 #218
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| # ── Quality gate ───────────────────────────────────────────────────── | |
| # Runs typecheck + unit tests on Ubuntu before any platform build starts, | |
| # so a broken tag never reaches the release matrix. One cheap job | |
| # instead of a separate `ci.yml` that fires on every push to main (that's | |
| # the EchoBird pattern — a separate push-triggered CI is what makes every | |
| # release look like "a big pile of CI"). Here the gate runs ONLY on tag | |
| # push / workflow_dispatch, gated behind `needs:`, so a normal | |
| # commit-to-main push pays zero CI cost. | |
| quality: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: src-ui/package-lock.json | |
| - name: Install frontend deps | |
| run: | | |
| cd src-ui | |
| npm ci | |
| # Full frontend build (tsc -b typecheck + vite build). The matrix | |
| # also builds the frontend, but repeating it here is ~1s and keeps | |
| # the gate self-contained + identical to local `npm run build`, so a | |
| # type error the matrix would hit is caught before any platform | |
| # runner spins up. NOTE: `npm run lint` is intentionally NOT gated | |
| # here — the repo has long-standing lint errors (empty catch blocks, | |
| # react-refresh exports) that predate this workflow and would block | |
| # every release until cleaned up. Add lint back once those are fixed. | |
| - name: Frontend build (typecheck) | |
| run: | | |
| cd src-ui | |
| npm run build | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| # Tauri's Linux build pulls glib-sys / gobject-sys / webkit2gtk-sys | |
| # crates, which pkg-config against the system dev libraries. The | |
| # release matrix installs these for the ubuntu builds; the quality | |
| # gate needs the SAME set or `cargo test` fails to compile the | |
| # glib-sys build script (glib-2.0.pc not found). Mirror the matrix's | |
| # install verbatim so the gate stays in lockstep with what actually | |
| # ships. | |
| - name: Install system dependencies (Linux) | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf | |
| - name: Rust cache | |
| uses: swatinem/rust-cache@v2 | |
| with: | |
| key: quality | |
| # Unit tests — includes the shell_probe detection / fallback-chain | |
| # suite. On Linux this also exercises the Unix cfg branches that | |
| # Windows-hosted dev never runs locally (#[cfg(not(target_os = | |
| # "windows"))] tests are filtered out on Windows). | |
| - name: Cargo test | |
| run: cargo test --no-fail-fast | |
| release: | |
| needs: quality | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: windows-latest | |
| args: '' | |
| label: Windows x64 | |
| - platform: macos-latest | |
| args: '--target aarch64-apple-darwin' | |
| target: aarch64-apple-darwin | |
| label: macOS Apple Silicon | |
| # GitHub Actions exposed a dedicated x86_64 macOS runner | |
| # (`macos-15-intel`) in May 2026, after the `macos-13` Intel runner | |
| # was deprecated. Without it, Intel Mac users had to run Coffee CLI | |
| # under Rosetta from the arm64 DMG. Now we ship a native x64 DMG too. | |
| - platform: macos-15-intel | |
| args: '--target x86_64-apple-darwin' | |
| target: x86_64-apple-darwin | |
| label: macOS Intel | |
| - platform: ubuntu-22.04 | |
| args: '' | |
| label: Linux x64 | |
| - platform: ubuntu-22.04-arm | |
| args: '' | |
| label: Linux arm64 | |
| runs-on: ${{ matrix.platform }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Resolve release tag | |
| id: tag | |
| shell: bash | |
| run: | | |
| # Tag-pushes give refs/tags/v1.2.3; workflow_dispatch falls back to | |
| # the latest tag in the working tree. Bare version (no leading "v") | |
| # is what tauri stamps into bundle filenames; both forms are | |
| # exposed for downstream steps. | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| if [ "$TAG" = "$GITHUB_REF" ]; then | |
| TAG=$(git describe --tags --abbrev=0) | |
| fi | |
| VERSION="${TAG#v}" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Install system dependencies (Linux) | |
| if: startsWith(matrix.platform, 'ubuntu-') | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf rpm | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: src-ui/package-lock.json | |
| - name: Install frontend deps | |
| run: | | |
| cd src-ui | |
| npm ci | |
| # All platforms build identically on stable xterm 6.0.0. The Linux CJK | |
| # IME punctuation-duplication bug (issue #37 / xtermjs#5374) is fixed in | |
| # the app layer instead — see TierTerminal.tsx (__IS_LINUX__-gated | |
| # suppression of xterm's composition events) — so there is no longer a | |
| # Linux-only dependency override or a Linux-only tsc-skip build path. | |
| - name: Build frontend (tsc + vite) | |
| run: | | |
| cd src-ui | |
| npm run build | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target || '' }} | |
| - name: Rust cache | |
| uses: swatinem/rust-cache@v2 | |
| with: | |
| key: ${{ matrix.label }} | |
| - name: Install Tauri CLI | |
| run: cargo install tauri-cli --version "^2.0" --locked | |
| # ── macOS: build .app, re-sign without hardened runtime, then bundle DMG ── | |
| # tauri-bundler's default codesign for adhoc identities adds | |
| # --options runtime, which without Apple notarization causes Gatekeeper | |
| # to silently reject the GUI launch (click icon, nothing happens). We | |
| # build the .app first, codesign --deep WITHOUT runtime, then bundle | |
| # the DMG from the re-signed .app. Gatekeeper accepts adhoc-signed | |
| # apps that aren't claiming hardened runtime. | |
| - name: Build .app only (macOS) | |
| if: runner.os == 'macOS' | |
| run: cargo tauri build --bundles app ${{ matrix.args }} | |
| - name: Re-sign without hardened runtime (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| APP=$(find target/${{ matrix.target }}/release/bundle/macos -maxdepth 2 -name "*.app" | head -1) | |
| if [ -z "$APP" ]; then | |
| echo "::error::No .app found" | |
| find target -name "*.app" 2>/dev/null | |
| exit 1 | |
| fi | |
| echo "Re-signing: $APP" | |
| codesign --force --deep --sign - "$APP" | |
| codesign -dv --verbose=4 "$APP" 2>&1 | |
| # Sanity assertion: no 'runtime' flag, sealed resources present. | |
| if codesign -dv --verbose=4 "$APP" 2>&1 | grep -qE "flags=.*runtime"; then | |
| echo "::error::runtime flag still present after re-sign" | |
| exit 1 | |
| fi | |
| if codesign -dv --verbose=4 "$APP" 2>&1 | grep -q "Sealed Resources=none"; then | |
| echo "::error::Sealed Resources=none after re-sign" | |
| exit 1 | |
| fi | |
| - name: Build DMG from re-signed .app (macOS) | |
| if: runner.os == 'macOS' | |
| run: cargo tauri bundle --bundles dmg ${{ matrix.args }} | |
| # ── Windows / Linux: standard one-shot bundle build ── | |
| - name: Build all bundles (Windows / Linux) | |
| if: runner.os != 'macOS' | |
| run: cargo tauri build ${{ matrix.args }} | |
| # ── Stage + rename artifacts to platform-labelled format ── | |
| # Tauri's bundler emits names like `Coffee CLI_1.9.2_amd64.deb` which | |
| # don't tell the end user which OS they're for and use inconsistent | |
| # arch slugs (amd64/x86_64/x64 across formats, plus rpm's `-1` release | |
| # suffix). We rename to the EchoBird-style convention: | |
| # Coffee.CLI_<version>_<OS>_<arch>.<ext> | |
| # so a glance at the GitHub Releases page makes the OS/arch obvious | |
| # and the install scripts have a single uniform pattern to grep. | |
| - name: Stage and rename artifacts | |
| id: stage | |
| shell: bash | |
| env: | |
| VERSION: ${{ steps.tag.outputs.version }} | |
| run: | | |
| set -e | |
| STAGING="$RUNNER_TEMP/coffee-release" | |
| mkdir -p "$STAGING" | |
| rename_one() { | |
| local src="$1" | |
| [ -f "$src" ] || return 0 | |
| local base | |
| base=$(basename "$src") | |
| local out="" | |
| case "$base" in | |
| # Windows | |
| *_x64-setup.exe) out="Coffee.CLI_${VERSION}_Windows_x64-setup.exe" ;; | |
| *_x64_*.msi|*_x64.msi) out="Coffee.CLI_${VERSION}_Windows_x64.msi" ;; | |
| # macOS — separate native DMGs for Apple Silicon and Intel. | |
| # Tauri's bundler uses `aarch64` / `x64` suffixes on the dmg | |
| # name (matching the target triple's first segment). | |
| *_aarch64.dmg) out="Coffee.CLI_${VERSION}_macOS_arm64.dmg" ;; | |
| *_x64.dmg) out="Coffee.CLI_${VERSION}_macOS_x64.dmg" ;; | |
| # Linux .deb (Tauri uses Debian "amd64"/"arm64") | |
| *_amd64.deb) out="Coffee.CLI_${VERSION}_Linux_x64.deb" ;; | |
| *_arm64.deb) out="Coffee.CLI_${VERSION}_Linux_arm64.deb" ;; | |
| # Linux .AppImage (upstream AppImage uses "amd64"/"aarch64") | |
| *_amd64.AppImage) out="Coffee.CLI_${VERSION}_Linux_x64.AppImage" ;; | |
| *_aarch64.AppImage) out="Coffee.CLI_${VERSION}_Linux_arm64.AppImage" ;; | |
| # Linux .rpm (uses dot-separated `<name>-<ver>-<release>.<arch>.rpm`) | |
| *.x86_64.rpm) out="Coffee.CLI_${VERSION}_Linux_x64.rpm" ;; | |
| *.aarch64.rpm) out="Coffee.CLI_${VERSION}_Linux_arm64.rpm" ;; | |
| *) echo " skip: $base"; return 0 ;; | |
| esac | |
| cp "$src" "$STAGING/$out" | |
| echo " staged: $base → $out" | |
| } | |
| echo "Bundle scan roots:" | |
| for root in \ | |
| target/release/bundle \ | |
| target/aarch64-apple-darwin/release/bundle \ | |
| target/x86_64-apple-darwin/release/bundle \ | |
| ; do | |
| [ -d "$root" ] || continue | |
| echo " $root" | |
| while IFS= read -r f; do | |
| rename_one "$f" | |
| done < <(find "$root" -type f \ | |
| \( -name "*.exe" -o -name "*.msi" \ | |
| -o -name "*.dmg" \ | |
| -o -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \) ) | |
| done | |
| echo "" | |
| echo "Final staged assets:" | |
| ls -la "$STAGING" | |
| echo "staging=$STAGING" >> "$GITHUB_OUTPUT" | |
| # ── Create release if missing, then upload all staged artifacts ── | |
| # Each matrix job races to create the release; `gh release create` | |
| # fails idempotently if another job already created it (caught and | |
| # ignored), then `gh release upload --clobber` overwrites any | |
| # earlier-job placeholder of the same filename. | |
| - name: Upload to GitHub release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -e | |
| TAG="${{ steps.tag.outputs.tag }}" | |
| STAGING="${{ steps.stage.outputs.staging }}" | |
| # Wait briefly in case another matrix job is mid-create. | |
| for i in 1 2 3 4 5 6; do | |
| if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then | |
| break | |
| fi | |
| echo "Release $TAG not yet present (attempt $i/6); waiting 10s..." | |
| sleep 10 | |
| done | |
| if ! gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then | |
| echo "Creating release $TAG (draft)" | |
| # Releases are created as drafts so the maintainer can review CI | |
| # artifacts and edit the changelog before users see the new | |
| # version. Clicking "Publish release" in the GitHub UI fires the | |
| # release.published event consumed by | |
| # .github/workflows/bump-version-json.yml, which bumps | |
| # Web-Home/version.json on main → CF Pages redeploys → in-app | |
| # update check picks up the new version. Until then, in-app | |
| # users and install scripts still see the previous release. | |
| NOTES_FILE="$GITHUB_WORKSPACE/install/release-notes-template.md" | |
| if [ -f "$NOTES_FILE" ]; then | |
| gh release create "$TAG" --repo "${{ github.repository }}" \ | |
| --draft \ | |
| --title "Coffee CLI $TAG" \ | |
| --notes-file "$NOTES_FILE" \ | |
| || echo "Release already exists (created by sibling matrix job)" | |
| else | |
| gh release create "$TAG" --repo "${{ github.repository }}" \ | |
| --draft \ | |
| --title "Coffee CLI $TAG" \ | |
| --notes "Coffee CLI $TAG. See README for install instructions." \ | |
| || echo "Release already exists (created by sibling matrix job)" | |
| fi | |
| fi | |
| # Upload everything in staging. --clobber overwrites if a previous | |
| # job already pushed an asset with the same name. | |
| shopt -s nullglob | |
| for f in "$STAGING"/*; do | |
| echo "uploading: $(basename "$f")" | |
| gh release upload "$TAG" "$f" --repo "${{ github.repository }}" --clobber | |
| done |