Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
386 changes: 193 additions & 193 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,193 +1,193 @@
name: Release

on:
push:
tags:
- 'v*'

# Opt into Node.js 24 runner for all JavaScript-based actions before the
# 2026-06-02 forced switch (mirrors the same opt-in in ci.yml).
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
# Create the GitHub release ONCE, up front, so the matrix build jobs below
# all upload to the SAME release object. Previously each matrix job ran
# tauri-action with tagName/releaseDraft, and since a DRAFT release is not
# returned by the releases/tags/{tag} lookup tauri-action uses, the parallel
# jobs each failed to "find" the draft and CREATED their own — producing
# duplicate drafts with the 8 assets split across them (hit on v5.3.3).
create-release:
permissions:
contents: write
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create.outputs.release_id }}
steps:
- name: Create (or reuse) the draft release
id: create
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
# Reuse an existing draft for this tag (safe on workflow re-runs),
# otherwise create one. Either path yields a single release id.
ID=$(gh api --paginate "repos/${REPO}/releases" \
--jq "[.[] | select(.tag_name == \"${TAG}\" and .draft == true)][0].id // empty")
if [ -z "$ID" ]; then
ID=$(gh api -X POST "repos/${REPO}/releases" \
-f tag_name="${TAG}" \
-f name="EchoBird ${TAG}" \
-f body="See the assets to download and install this version." \
-F draft=true -F prerelease=false --jq '.id')
echo "created draft release $ID"
else
echo "reusing existing draft release $ID"
fi
echo "release_id=$ID" >> "$GITHUB_OUTPUT"

release:
needs: create-release
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'windows-latest'
rust-target: 'x86_64-pc-windows-msvc'
args: ''

# macOS: native build per architecture. Rosetta 2 only runs the
# OTHER way (Intel binaries on ARM Macs) — Intel Macs cannot
# run ARM-only apps, so we ship both. macos-latest is ARM,
# macos-15-intel is GitHub's current Intel runner label
# (replaced macos-13, which was fully unsupported on 2025-12-08
# per https://github.blog/changelog/2025-09-19-... — the v4.7.3
# tag briefly used `macos-13` and its job queued indefinitely
# because the runner image was gone; we burned v4.7.3 over that
# and jumped straight to v4.7.4).
- platform: 'macos-latest'
rust-target: 'aarch64-apple-darwin'
args: '--target aarch64-apple-darwin --bundles dmg'

- platform: 'macos-15-intel'
rust-target: 'x86_64-apple-darwin'
args: '--target x86_64-apple-darwin --bundles dmg'

# Linux: deb + rpm only. AppImage bundles full webkit/gtk runtime
# (~80MB each); deb/rpm rely on system webkit2gtk (~11MB each).
- platform: 'ubuntu-latest'
rust-target: 'x86_64-unknown-linux-gnu'
args: '--bundles deb,rpm'

- platform: 'ubuntu-24.04-arm'
rust-target: 'aarch64-unknown-linux-gnu'
args: '--bundles deb,rpm'

runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
# Pinned to 22 to match ci.yml. setup-node@v6 + node-version: lts/*
# triggers "manifest.filter is not a function" on Node 24 runners
# (Windows + Linux x86_64 hit this on tag v4.9.4 release run
# 26334424977 — 2026-05-23). ci.yml never hit it because it always
# used a numeric pin.
node-version: 22
cache: 'npm'

# Pinned to 1.93 — see comment in ci.yml. Keep both files in
# lockstep so a deliberate bump touches both.
- name: Install Rust 1.93 (pinned)
uses: dtolnay/rust-toolchain@1.93
with:
targets: ${{ matrix.rust-target }}

- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'

- name: Install Linux system dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libssl-dev libgtk-3-dev

- name: Install frontend dependencies
run: npm ci

- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Upload to the single draft created by `create-release` above.
# Using releaseId (instead of tagName/releaseDraft) makes tauri-action
# upload to the EXISTING release rather than create its own — this is
# what removes the duplicate-draft race across the matrix. The release
# stays a draft (created that way) until the operator flips it public
# as the last step via `gh release edit <tag> --draft=false`.
releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.args }}

rename-assets:
# Tauri's bundler uses inconsistent filenames (no platform label, RPM "-1"
# release counter, version-less updater bundle). Rename every asset on the
# release in place via the GitHub API — non-destructive, no re-upload.
needs: release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Rename release assets with platform labels
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
VER="${TAG#v}"
# Pull every asset on this tag, derive a clean platform-labeled
# name, PATCH only when the name actually changes.
#
# `gh api releases/tags/${TAG}` does NOT return draft releases —
# since v4.8.6 we publish as draft (releaseDraft: true) and flip
# to public manually as the last step, so we have to list all
# releases and filter ourselves (`gh release view` also works
# for drafts but doesn't give us numeric asset IDs we need for
# the PATCH call below).
gh api --paginate "repos/${REPO}/releases" \
--jq ".[] | select(.tag_name == \"${TAG}\") | .assets[] | \"\(.id)\t\(.name)\"" \
| while IFS=$'\t' read -r ID NAME; do
case "$NAME" in
*_x64-setup.exe) NEW="EchoBird_${VER}_Windows_x64-setup.exe" ;;
*_x64_*.msi) NEW="EchoBird_${VER}_Windows_x64.msi" ;;
*_aarch64.dmg) NEW="EchoBird_${VER}_macOS_arm64.dmg" ;;
*_x64.dmg) NEW="EchoBird_${VER}_macOS_x64.dmg" ;;
*_amd64.deb) NEW="EchoBird_${VER}_Linux_x64.deb" ;;
*_arm64.deb) NEW="EchoBird_${VER}_Linux_arm64.deb" ;;
*.x86_64.rpm) NEW="EchoBird_${VER}_Linux_x64.rpm" ;;
*.aarch64.rpm) NEW="EchoBird_${VER}_Linux_arm64.rpm" ;;
*) NEW="" ;;
esac
if [ -n "$NEW" ] && [ "$NEW" != "$NAME" ]; then
echo "rename: $NAME -> $NEW"
gh api -X PATCH "repos/${REPO}/releases/assets/${ID}" -f name="$NEW" --jq '.name'
fi
done

# NOTE: manifest bump moved to .github/workflows/sync-version-manifest.yml.
# That workflow triggers on `release: { types: [published] }`, which fires
# the moment the operator flips draft → public (the final step of the
# release flow). Bumping the manifest from this tag-push workflow used
# to race the publish: a 5-15 min window where in-app auto-updater told
# users "v4.8.6 available" but clicking download 404'd because the
# GitHub Release was still draft.
name: Release
on:
push:
tags:
- 'v*'
# Opt into Node.js 24 runner for all JavaScript-based actions before the
# 2026-06-02 forced switch (mirrors the same opt-in in ci.yml).
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Create the GitHub release ONCE, up front, so the matrix build jobs below
# all upload to the SAME release object. Previously each matrix job ran
# tauri-action with tagName/releaseDraft, and since a DRAFT release is not
# returned by the releases/tags/{tag} lookup tauri-action uses, the parallel
# jobs each failed to "find" the draft and CREATED their own — producing
# duplicate drafts with the 8 assets split across them (hit on v5.3.3).
create-release:
permissions:
contents: write
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create.outputs.release_id }}
steps:
- name: Create (or reuse) the draft release
id: create
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
# Reuse an existing draft for this tag (safe on workflow re-runs),
# otherwise create one. Either path yields a single release id.
ID=$(gh api --paginate "repos/${REPO}/releases" \
--jq "[.[] | select(.tag_name == \"${TAG}\" and .draft == true)][0].id // empty")
if [ -z "$ID" ]; then
ID=$(gh api -X POST "repos/${REPO}/releases" \
-f tag_name="${TAG}" \
-f name="EchoBird ${TAG}" \
-f body="See the assets to download and install this version." \
-F draft=true -F prerelease=false --jq '.id')
echo "created draft release $ID"
else
echo "reusing existing draft release $ID"
fi
echo "release_id=$ID" >> "$GITHUB_OUTPUT"
release:
needs: create-release
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'windows-latest'
rust-target: 'x86_64-pc-windows-msvc'
args: ''
# macOS: native build per architecture. Rosetta 2 only runs the
# OTHER way (Intel binaries on ARM Macs) — Intel Macs cannot
# run ARM-only apps, so we ship both. macos-latest is ARM,
# macos-15-intel is GitHub's current Intel runner label
# (replaced macos-13, which was fully unsupported on 2025-12-08
# per https://github.blog/changelog/2025-09-19-... — the v4.7.3
# tag briefly used `macos-13` and its job queued indefinitely
# because the runner image was gone; we burned v4.7.3 over that
# and jumped straight to v4.7.4).
- platform: 'macos-latest'
rust-target: 'aarch64-apple-darwin'
args: '--target aarch64-apple-darwin --bundles dmg'
- platform: 'macos-15-intel'
rust-target: 'x86_64-apple-darwin'
args: '--target x86_64-apple-darwin --bundles dmg'
# Linux: deb + rpm only. AppImage bundles full webkit/gtk runtime
# (~80MB each); deb/rpm rely on system webkit2gtk (~11MB each).
- platform: 'ubuntu-latest'
rust-target: 'x86_64-unknown-linux-gnu'
args: '--bundles deb,rpm'
- platform: 'ubuntu-24.04-arm'
rust-target: 'aarch64-unknown-linux-gnu'
args: '--bundles deb,rpm'
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
# Pinned to 22 to match ci.yml. setup-node@v6 + node-version: lts/*
# triggers "manifest.filter is not a function" on Node 24 runners
# (Windows + Linux x86_64 hit this on tag v4.9.4 release run
# 26334424977 — 2026-05-23). ci.yml never hit it because it always
# used a numeric pin.
node-version: 22
cache: 'npm'
# Pinned to 1.93 — see comment in ci.yml. Keep both files in
# lockstep so a deliberate bump touches both.
- name: Install Rust 1.93 (pinned)
uses: dtolnay/rust-toolchain@1.93
with:
targets: ${{ matrix.rust-target }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: Install Linux system dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libssl-dev libgtk-3-dev
- name: Install frontend dependencies
run: npm ci
- name: Build the app
uses: tauri-apps/tauri-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Upload to the single draft created by `create-release` above.
# Using releaseId (instead of tagName/releaseDraft) makes tauri-action
# upload to the EXISTING release rather than create its own — this is
# what removes the duplicate-draft race across the matrix. The release
# stays a draft (created that way) until the operator flips it public
# as the last step via `gh release edit <tag> --draft=false`.
releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.args }}
rename-assets:
# Tauri's bundler uses inconsistent filenames (no platform label, RPM "-1"
# release counter, version-less updater bundle). Rename every asset on the
# release in place via the GitHub API — non-destructive, no re-upload.
needs: release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Rename release assets with platform labels
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
VER="${TAG#v}"
# Pull every asset on this tag, derive a clean platform-labeled
# name, PATCH only when the name actually changes.
#
# `gh api releases/tags/${TAG}` does NOT return draft releases —
# since v4.8.6 we publish as draft (releaseDraft: true) and flip
# to public manually as the last step, so we have to list all
# releases and filter ourselves (`gh release view` also works
# for drafts but doesn't give us numeric asset IDs we need for
# the PATCH call below).
gh api --paginate "repos/${REPO}/releases" \
--jq ".[] | select(.tag_name == \"${TAG}\") | .assets[] | \"\(.id)\t\(.name)\"" \
| while IFS=$'\t' read -r ID NAME; do
case "$NAME" in
*_x64-setup.exe) NEW="EchoBird_${VER}_Windows_x64-setup.exe" ;;
*_x64_*.msi) NEW="EchoBird_${VER}_Windows_x64.msi" ;;
*_aarch64.dmg) NEW="EchoBird_${VER}_macOS_arm64.dmg" ;;
*_x64.dmg) NEW="EchoBird_${VER}_macOS_x64.dmg" ;;
*_amd64.deb) NEW="EchoBird_${VER}_Linux_x64.deb" ;;
*_arm64.deb) NEW="EchoBird_${VER}_Linux_arm64.deb" ;;
*.x86_64.rpm) NEW="EchoBird_${VER}_Linux_x64.rpm" ;;
*.aarch64.rpm) NEW="EchoBird_${VER}_Linux_arm64.rpm" ;;
*) NEW="" ;;
esac
if [ -n "$NEW" ] && [ "$NEW" != "$NAME" ]; then
echo "rename: $NAME -> $NEW"
gh api -X PATCH "repos/${REPO}/releases/assets/${ID}" -f name="$NEW" --jq '.name'
fi
done
# NOTE: manifest bump moved to .github/workflows/sync-version-manifest.yml.
# That workflow triggers on `release: { types: [published] }`, which fires
# the moment the operator flips draft → public (the final step of the
# release flow). Bumping the manifest from this tag-push workflow used
# to race the publish: a 5-15 min window where in-app auto-updater told
# users "v4.8.6 available" but clicking download 404'd because the
# GitHub Release was still draft.