Skip to content

test(announcements): update release version expectations #24

test(announcements): update release version expectations

test(announcements): update release version expectations #24

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
permissions:
contents: read
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: true
jobs:
ci:
name: CI
uses: ./.github/workflows/ci.yml
validate:
name: Validate release version
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
- name: Validate release version
run: node scripts/validate-release-version.mjs
- name: Test Node scripts
run: >-
node --test
scripts/dev.node.mjs
scripts/generate-updater-manifest.node.mjs
scripts/build-fast.node.mjs
scripts/verify-bundle-marker.node.mjs
- name: Validate updater signing secret
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: |
set -euo pipefail
if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then
printf 'Missing required repository secret: TAURI_SIGNING_PRIVATE_KEY\n' >&2
exit 1
fi
release:
needs: [ci, validate]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: windows-2022
rust-target: x86_64-pc-windows-msvc
- os: windows-11-arm
arch: arm64
rust-target: aarch64-pc-windows-msvc
- os: ubuntu-22.04
rust-target: x86_64-unknown-linux-gnu
- os: ubuntu-22.04-arm
arch: arm64
rust-target: aarch64-unknown-linux-gnu
- os: macos-14
rust-target: universal-apple-darwin
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
- name: Add Windows ARM64 target
if: runner.os == 'Windows' && matrix.arch == 'arm64'
shell: pwsh
run: rustup target add aarch64-pc-windows-msvc
- name: Add macOS targets
if: runner.os == 'macOS'
run: |
rustup target add aarch64-apple-darwin x86_64-apple-darwin
- name: Cache Rust build
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri -> target
shared-key: release-${{ runner.os }}-${{ runner.arch }}-${{ matrix.rust-target }}
key: ${{ runner.arch }}-${{ matrix.rust-target }}
cache-on-failure: true
- name: Install Linux system deps
if: runner.os == 'Linux'
shell: bash
run: |
set -euxo pipefail
sudo apt-get update
# Core build tools and pkg-config
sudo apt-get install -y --no-install-recommends \
build-essential \
pkg-config \
curl \
wget \
file \
patchelf \
libssl-dev \
rpm \
flatpak \
flatpak-builder \
elfutils \
xdg-utils
# GTK/GLib stack for gdk-3.0, glib-2.0, gio-2.0
sudo apt-get install -y --no-install-recommends \
libgtk-3-dev \
librsvg2-dev \
libayatana-appindicator3-dev
# WebKit2GTK (version differs across Ubuntu images; try 4.1 then 4.0)
sudo apt-get install -y --no-install-recommends libwebkit2gtk-4.1-dev \
|| sudo apt-get install -y --no-install-recommends libwebkit2gtk-4.0-dev
# libsoup also changed major version; prefer 3.0 with fallback to 2.4
sudo apt-get install -y --no-install-recommends libsoup-3.0-dev \
|| sudo apt-get install -y --no-install-recommends libsoup2.4-dev
- name: Setup pnpm
if: runner.os != 'Windows' || matrix.arch != 'arm64'
uses: pnpm/action-setup@v6
with:
version: 10.12.3
run_install: false
- name: Setup Node.js (Windows ARM64)
if: runner.os == 'Windows' && matrix.arch == 'arm64'
uses: actions/setup-node@v6
with:
node-version: "20"
- name: Setup pnpm (Windows ARM64)
if: runner.os == 'Windows' && matrix.arch == 'arm64'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
corepack enable
corepack prepare pnpm@10.12.3 --activate
node --version
pnpm --version
- name: Setup Node.js
if: runner.os != 'Windows' || matrix.arch != 'arm64'
uses: actions/setup-node@v6
with:
node-version: "20"
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Setup pnpm cache (Windows ARM64)
if: runner.os == 'Windows' && matrix.arch == 'arm64'
uses: actions/setup-node@v6
with:
node-version: "20"
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Setup LLVM for Windows ARM64
if: runner.os == 'Windows' && matrix.arch == 'arm64'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$llvmRoot = 'C:\Program Files\LLVM'
if (-not (Test-Path $llvmRoot)) {
throw "LLVM not found at $llvmRoot"
}
$llvmBin = Join-Path $llvmRoot 'bin'
"LIBCLANG_PATH=$llvmBin" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"CLANG_PATH=$(Join-Path $llvmBin 'clang.exe')" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
$llvmBin | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
- name: Install frontend deps
run: pnpm install --frozen-lockfile
- name: Build Tauri App (macOS, ad-hoc signed)
if: runner.os == 'macOS'
shell: bash
timeout-minutes: 60
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: pnpm tauri build --target universal-apple-darwin
- name: Build Tauri App (Windows)
if: runner.os == 'Windows'
shell: pwsh
timeout-minutes: 60
env:
WINDOWS_RELEASE_ARCH: ${{ matrix.arch || 'x86_64' }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: |
$ErrorActionPreference = 'Stop'
if ($env:WINDOWS_RELEASE_ARCH -eq 'arm64') {
pnpm tauri build --target aarch64-pc-windows-msvc --bundles msi
} else {
pnpm tauri build --bundles msi
}
- name: Build Tauri App (Linux)
if: runner.os == 'Linux'
timeout-minutes: 60
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: pnpm tauri build --bundles appimage,deb,rpm
- name: Verify Linux DEB bundle marker
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
DEB=$(find src-tauri/target/release/bundle -type f -name "*.deb" -print -quit)
if [ -z "$DEB" ]; then
echo "No .deb found for bundle marker verification" >&2
exit 1
fi
EXTRACT_DIR=$(mktemp -d)
trap 'rm -rf "$EXTRACT_DIR"' EXIT
dpkg-deb --extract "$DEB" "$EXTRACT_DIR"
DEB_BINARY=$(find "$EXTRACT_DIR/usr/bin" -maxdepth 1 -type f -print -quit)
if [ -z "$DEB_BINARY" ]; then
echo "No executable found in $DEB" >&2
exit 1
fi
node scripts/verify-bundle-marker.mjs "$DEB_BINARY" deb
- name: Prepare macOS Assets
if: runner.os == 'macOS'
shell: bash
run: |
set -euxo pipefail
mkdir -p release-assets
VERSION="${GITHUB_REF_NAME}" # e.g., v3.5.0
# Locate bundle artifacts
APP_PATH=""
TAR_GZ=""
for path in \
"src-tauri/target/universal-apple-darwin/release/bundle/macos" \
"src-tauri/target/aarch64-apple-darwin/release/bundle/macos" \
"src-tauri/target/x86_64-apple-darwin/release/bundle/macos" \
"src-tauri/target/release/bundle/macos"; do
if [ -d "$path" ]; then
[ -z "$APP_PATH" ] && APP_PATH=$(find "$path" -maxdepth 1 -name "*.app" -type d | head -1 || true)
[ -z "$TAR_GZ" ] && TAR_GZ=$(find "$path" -maxdepth 1 -name "*.tar.gz" -type f | head -1 || true)
fi
done
if [ -z "$APP_PATH" ]; then
echo "❌ No .app found" >&2
exit 1
fi
if [ -z "$TAR_GZ" ] || [ ! -f "$TAR_GZ.sig" ]; then
echo "No signed macOS updater artifact found" >&2
exit 1
fi
NEW_TAR_GZ="StackFerry-${VERSION}-macOS.tar.gz"
cp "$TAR_GZ" "release-assets/$NEW_TAR_GZ"
cp "$TAR_GZ.sig" "release-assets/$NEW_TAR_GZ.sig"
NEW_ZIP="StackFerry-${VERSION}-macOS.zip"
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "release-assets/$NEW_ZIP"
echo "macOS zip ready: $NEW_ZIP"
HOMEBREW_NO_AUTO_UPDATE=1 brew install create-dmg
NEW_DMG="StackFerry-${VERSION}-macOS.dmg"
DMG_STAGE_DIR="$RUNNER_TEMP/dmg-stage"
rm -rf "$DMG_STAGE_DIR"
mkdir -p "$DMG_STAGE_DIR"
ditto "$APP_PATH" "$DMG_STAGE_DIR/StackFerry.app"
create-dmg \
--volname "StackFerry" \
--background "src-tauri/icons/dmg-background.png" \
--window-size 660 400 \
--window-pos 200 120 \
--icon-size 80 \
--icon "StackFerry.app" 180 220 \
--hide-extension "StackFerry.app" \
--app-drop-link 480 220 \
--no-internet-enable \
"release-assets/$NEW_DMG" \
"$DMG_STAGE_DIR"
rm -rf "$DMG_STAGE_DIR"
echo "✅ Styled DMG created: $NEW_DMG"
- name: Verify macOS artifacts
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
APP_PATH=""
for path in \
"src-tauri/target/universal-apple-darwin/release/bundle/macos" \
"src-tauri/target/aarch64-apple-darwin/release/bundle/macos" \
"src-tauri/target/x86_64-apple-darwin/release/bundle/macos" \
"src-tauri/target/release/bundle/macos"; do
if [ -d "$path" ]; then
[ -z "$APP_PATH" ] && APP_PATH=$(find "$path" -maxdepth 1 -name "*.app" -type d | head -1 || true)
fi
done
if [ -z "$APP_PATH" ]; then
echo "❌ No .app found for verification" >&2
exit 1
fi
echo "=== Verifying .app: $APP_PATH ==="
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
echo "✅ codesign verification passed"
DMG_PATH=$(find release-assets -maxdepth 1 -name "*.dmg" -type f | head -1 || true)
if [ -n "$DMG_PATH" ]; then
echo "=== Verifying .dmg: $DMG_PATH ==="
hdiutil verify "$DMG_PATH"
echo "✅ .dmg integrity verification passed"
else
echo "❌ No .dmg found for verification" >&2
exit 1
fi
- name: Prepare Windows Assets
if: runner.os == 'Windows'
shell: pwsh
env:
WINDOWS_RELEASE_ARCH: ${{ matrix.arch || 'x86_64' }}
run: |
$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Force -Path release-assets | Out-Null
$VERSION = $env:GITHUB_REF_NAME # e.g., v3.5.0
$isArm64 = $env:WINDOWS_RELEASE_ARCH -eq 'arm64'
$targetRoot = if ($isArm64) { 'src-tauri/target/aarch64-pc-windows-msvc/release' } else { 'src-tauri/target/release' }
$assetSuffix = if ($isArm64) { '-arm64' } else { '' }
# 打包 MSI 安装器
$msi = Get-ChildItem -Path (Join-Path $targetRoot 'bundle/msi') -Recurse -Include *.msi -ErrorAction SilentlyContinue | Select-Object -First 1
if ($null -eq $msi) {
# 兜底:全局搜索 .msi
$msi = Get-ChildItem -Path (Join-Path $targetRoot 'bundle') -Recurse -Include *.msi -ErrorAction SilentlyContinue | Select-Object -First 1
}
if ($null -ne $msi) {
$dest = "StackFerry-$VERSION-Windows$assetSuffix.msi"
Copy-Item $msi.FullName (Join-Path release-assets $dest)
$signature = "$($msi.FullName).sig"
if (-not (Test-Path $signature)) {
throw "No updater signature found for $($msi.FullName)"
}
Copy-Item $signature (Join-Path release-assets "$dest.sig")
Write-Host "Installer copied: $dest"
} elseif ($isArm64) {
throw 'No Windows ARM64 MSI installer found'
} else {
Write-Warning 'No Windows MSI installer found'
}
# 绿色版(portable):仅可执行文件打 zip(不参与 Updater)
$exeCandidates = if ($isArm64) {
@('src-tauri/target/aarch64-pc-windows-msvc/release/stackferry.exe')
} else {
@(
'src-tauri/target/release/stackferry.exe',
'src-tauri/target/x86_64-pc-windows-msvc/release/stackferry.exe'
)
}
$exePath = $exeCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($null -ne $exePath) {
$portableDir = 'release-assets/StackFerry-Portable'
New-Item -ItemType Directory -Force -Path $portableDir | Out-Null
Copy-Item $exePath $portableDir
$portableIniPath = Join-Path $portableDir 'portable.ini'
$portableContent = if ($isArm64) {
@(
'# StackFerry portable ARM64 build marker',
'portable=true',
'arch=arm64'
)
} else {
@(
'# StackFerry portable build marker',
'portable=true'
)
}
$portableContent | Set-Content -Path $portableIniPath -Encoding UTF8
$portableZip = "release-assets/StackFerry-$VERSION-Windows$assetSuffix-Portable.zip"
Compress-Archive -Path "$portableDir/*" -DestinationPath $portableZip -Force
Remove-Item -Recurse -Force $portableDir
Write-Host "Windows portable zip created: StackFerry-$VERSION-Windows$assetSuffix-Portable.zip"
} elseif ($isArm64) {
throw 'Portable ARM64 exe not found'
} else {
Write-Warning 'Portable exe not found'
}
- name: Prepare Linux Assets
if: runner.os == 'Linux'
shell: bash
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: |
set -euxo pipefail
mkdir -p release-assets
VERSION="${GITHUB_REF_NAME}" # e.g., v3.5.0
ARCH="${{ matrix.arch || 'x86_64' }}"
copy_signed_artifact() {
local artifact="$1"
local destination="$2"
if [ ! -f "$artifact.sig" ]; then
pnpm tauri signer sign "$artifact"
fi
if [ ! -s "$artifact.sig" ]; then
echo "No updater signature found for $artifact" >&2
exit 1
fi
cp "$artifact" "release-assets/$destination"
cp "$artifact.sig" "release-assets/$destination.sig"
}
# AppImage
APPIMAGE=$(find src-tauri/target/release/bundle -name "*.AppImage" | head -1 || true)
if [ -n "$APPIMAGE" ]; then
NEW_APPIMAGE="StackFerry-${VERSION}-Linux-${ARCH}.AppImage"
copy_signed_artifact "$APPIMAGE" "$NEW_APPIMAGE"
echo "AppImage copied: $NEW_APPIMAGE"
else
echo "No AppImage found under target/release/bundle" >&2
exit 1
fi
# deb updater artifact
DEB=$(find src-tauri/target/release/bundle -name "*.deb" | head -1 || true)
if [ -n "$DEB" ]; then
NEW_DEB="StackFerry-${VERSION}-Linux-${ARCH}.deb"
copy_signed_artifact "$DEB" "$NEW_DEB"
echo "Deb package copied: $NEW_DEB"
else
echo "No .deb found under target/release/bundle" >&2
exit 1
fi
# rpm updater artifact
RPM=$(find src-tauri/target/release/bundle -name "*.rpm" | head -1 || true)
if [ -n "$RPM" ]; then
NEW_RPM="StackFerry-${VERSION}-Linux-${ARCH}.rpm"
copy_signed_artifact "$RPM" "$NEW_RPM"
echo "RPM package copied: $NEW_RPM"
else
echo "No .rpm found under target/release/bundle" >&2
exit 1
fi
- name: List prepared assets
shell: bash
run: |
ls -la release-assets || true
- name: Upload release artifacts to workflow
uses: actions/upload-artifact@v7
with:
name: release-assets-${{ runner.os }}-${{ matrix.arch || runner.arch }}
path: release-assets/*
if-no-files-found: error
- name: List generated bundles (debug)
if: always()
shell: bash
run: |
echo "Listing bundles in src-tauri/target..."
find src-tauri/target -maxdepth 4 -type f -name "*.*" 2>/dev/null || true
publish-release:
name: Publish GitHub Release
runs-on: ubuntu-22.04
needs: release
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
- name: Download built release artifacts
uses: actions/download-artifact@v8
with:
pattern: release-assets-*
path: release-assets
merge-multiple: true
- name: List downloaded release artifacts
shell: bash
run: |
set -euo pipefail
ls -la release-assets
- name: Generate release notes
id: release-notes
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$ErrorActionPreference = "Stop"
git fetch origin --tags --force
$tag = "${{ github.ref_name }}"
$stableTagPattern = '^v\d+\.\d+\.\d+$'
$previousTag = git tag --list --sort=-version:refname |
Where-Object {
$_ -match $stableTagPattern -and $_ -ne $tag
} |
Where-Object {
git merge-base --is-ancestor $_ $tag 2>$null
$LASTEXITCODE -eq 0
} |
Select-Object -First 1
if ($previousTag) {
$range = "$previousTag..$tag"
} else {
$range = $tag
}
$repository = "${{ github.repository }}"
$commitRows = @(git log $range --format="%H%x09%h%x09%s")
function Get-GitHubMention([string]$commitHash) {
$headers = @{
Accept = "application/vnd.github+json"
Authorization = "Bearer $env:GITHUB_TOKEN"
"X-GitHub-Api-Version" = "2022-11-28"
"User-Agent" = "StackFerry-release-workflow"
}
try {
$commit = Invoke-RestMethod -Headers $headers -Uri "https://api.github.com/repos/$repository/commits/$commitHash"
if ($commit.author -and $commit.author.login) {
return "@$($commit.author.login)"
}
} catch {
}
return $null
}
$body = [System.Collections.Generic.List[string]]::new()
function Add-MarkdownBody([string]$path) {
if (-not (Test-Path $path)) {
return
}
$content = @(Get-Content $path)
if ($content.Count -eq 0) {
return
}
$startIndex = 0
if ($content[0] -match '^#\s+') {
$startIndex = 1
while ($startIndex -lt $content.Count -and [string]::IsNullOrWhiteSpace($content[$startIndex])) {
$startIndex++
}
}
for ($index = $startIndex; $index -lt $content.Count; $index++) {
$body.Add([string]$content[$index])
}
}
Add-MarkdownBody "RELEASE.md"
if ($body.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($body[$body.Count - 1])) {
$body.Add("")
}
$body.Add("---")
$body.Add("")
$body.Add("### Commits")
$body.Add("")
foreach ($row in $commitRows) {
if ([string]::IsNullOrWhiteSpace($row)) {
continue
}
$parts = $row -split "`t", 3
if ($parts.Count -lt 3) {
continue
}
$fullHash = $parts[0]
$shortHash = $parts[1]
$subject = $parts[2]
$mention = Get-GitHubMention $fullHash
if ($mention) {
$body.Add("- ${subject}(${shortHash}) $mention")
} else {
$body.Add("- ${subject}(${shortHash})")
}
}
$notesPath = Join-Path $env:RUNNER_TEMP "release-notes.md"
$body | Set-Content $notesPath -Encoding utf8
"path=$notesPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Generate updater manifest
shell: bash
run: |
set -euo pipefail
published_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
node scripts/generate-updater-manifest.mjs \
--assets-dir release-assets \
--tag "$GITHUB_REF_NAME" \
--repository "$GITHUB_REPOSITORY" \
--published-at "$published_at"
- name: Upload Release Assets
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: StackFerry ${{ github.ref_name }}
prerelease: ${{ contains(github.ref_name, '-') }}
body_path: ${{ steps.release-notes.outputs.path }}
files: release-assets/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}