Skip to content

chore(release): v2.6.0 #160

chore(release): v2.6.0

chore(release): v2.6.0 #160

Workflow file for this run

name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
release:
strategy:
fail-fast: false
matrix:
include:
- platform: windows-latest
args: ''
label: Windows x64
- platform: macos-latest
args: '--target aarch64-apple-darwin'
label: macOS Apple Silicon
- 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 & build frontend
run: |
cd src-ui
npm ci
npm run build
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ contains(matrix.args, 'aarch64-apple-darwin') && 'aarch64-apple-darwin' || '' }}
- 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: matrix.platform == 'macos-latest'
run: cargo tauri build --bundles app ${{ matrix.args }}
- name: Re-sign without hardened runtime (macOS)
if: matrix.platform == 'macos-latest'
run: |
APP=$(find target/aarch64-apple-darwin/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: matrix.platform == 'macos-latest'
run: cargo tauri bundle --bundles dmg ${{ matrix.args }}
# ── Windows / Linux: standard one-shot bundle build ──
- name: Build all bundles (Windows / Linux)
if: matrix.platform != 'macos-latest'
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 (Coffee CLI ships arm64 only; Intel runs via Rosetta)
*_aarch64.dmg) out="Coffee.CLI_${VERSION}_macOS_arm64.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 \
; 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"
# Notes body lives in install/release-notes-template.md so the
# YAML block doesn't have to fight bash heredoc indentation
# rules. `gh release create --notes-file` accepts a path; we
# don't need to interpolate the tag because the body is the
# generic install instructions, not version-specific changelog.
NOTES_FILE="$GITHUB_WORKSPACE/install/release-notes-template.md"
if [ -f "$NOTES_FILE" ]; then
gh release create "$TAG" --repo "${{ github.repository }}" \
--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 }}" \
--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