Files
ChatLab/.github/workflows/release.yml
T
digua 860bf52d62 feat(analytics): add privacy-safe product telemetry
Add self-hosted Umami reporting alongside existing telemetry across Desktop and CLI Web.

Track normalized startup, import, feature, and AI usage with a stable anonymous ID and strict privacy allowlists. Keep incremental usage platform-independent and preserve accurate failure classifications and platform metadata across import paths.
2026-07-29 14:28:48 +08:00

623 lines
22 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Build and Release
on:
# 手动触发
workflow_dispatch:
inputs:
version:
description: '版本号 (如 0.1.0)'
required: true
type: string
# 推送 tag 时自动触发
push:
tags:
- 'v*'
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
prepare_release:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.meta.outputs.version }}
version_tag: ${{ steps.meta.outputs.version_tag }}
npm_tag: ${{ steps.meta.outputs.npm_tag }}
is_prerelease: ${{ steps.meta.outputs.is_prerelease }}
steps:
- name: Determine release metadata
id: meta
shell: bash
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
VERSION="${VERSION#v}"
else
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}"
fi
VERSION_TAG="v${VERSION}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "version_tag=$VERSION_TAG" >> "$GITHUB_OUTPUT"
if [[ "$VERSION" =~ -(beta|alpha|rc)(\.|$) ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
if [[ "$VERSION" =~ -beta(\.|$) ]]; then
echo "npm_tag=beta" >> "$GITHUB_OUTPUT"
elif [[ "$VERSION" =~ -alpha(\.|$) ]]; then
echo "npm_tag=alpha" >> "$GITHUB_OUTPUT"
elif [[ "$VERSION" =~ -rc(\.|$) ]]; then
echo "npm_tag=rc" >> "$GITHUB_OUTPUT"
else
echo "npm_tag=latest" >> "$GITHUB_OUTPUT"
fi
echo "Release version: $VERSION_TAG"
# macOS 构建需要分架构
# better-sqlite3 等原生模块会通过 prebuild 下载对应架构的预编译二进制
build-mac:
needs: [prepare_release]
strategy:
matrix:
include:
- os: macos-14 # 使用 ARM runner 交叉编译 x64
arch: x64
- os: macos-14 # Apple Silicon (arm64) 原生构建
arch: arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '24'
package-manager-cache: false
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-${{ matrix.arch }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-${{ matrix.arch }}-pnpm-store-
- name: Install dependencies
run: |
echo "node-linker=hoisted" >> .npmrc
pnpm install --frozen-lockfile
# Rust native parser:目标架构与 electron-builder --x64/--arm64 保持一致
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.arch == 'x64' && 'x86_64-apple-darwin' || 'aarch64-apple-darwin' }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: packages/parser-native
key: ${{ matrix.arch }}
# --target 必须直接传给 napi(不能用 -- 透传给 cargo),
# 否则 napi 感知不到交叉编译,会按宿主架构(arm64)命名并构建产物
- name: Build native parser (${{ matrix.arch }})
shell: bash
run: |
if [ "${{ matrix.arch }}" == "x64" ]; then
pnpm --filter @openchatlab/parser-native build --target x86_64-apple-darwin
else
pnpm --filter @openchatlab/parser-native build
fi
- name: Verify native parser artifact
shell: bash
run: ls -la packages/parser-native/chatlab-parser-native.darwin-${{ matrix.arch }}.node
- name: Inject version into app package.json
shell: bash
run: |
VERSION="${{ needs.prepare_release.outputs.version }}"
echo "Injecting version: $VERSION"
node -e "
const fs = require('fs');
for (const p of ['apps/desktop/package.json', 'apps/cli/package.json']) {
const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
pkg.version = '${VERSION}';
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
console.log(p + ' → ' + '${VERSION}');
}
"
- name: Cache Electron and electron-builder binaries
uses: actions/cache@v5
with:
path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
key: ${{ runner.os }}-${{ matrix.arch }}-electron-builder-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-${{ matrix.arch }}-electron-builder-
# macOS 签名和公证需要的 API Key 文件
- name: Create Apple API Key File
run: |
mkdir -p ~/private_keys
echo "${{ secrets.APPLE_API_KEY }}" > ~/private_keys/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8
- name: Build Electron app for macOS (${{ matrix.arch }})
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
# 代码签名
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
# 公证
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
# 分析服务
UMAMI_ENDPOINT: ${{ secrets.UMAMI_ENDPOINT }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID }}
working-directory: apps/desktop
run: pnpm run build && pnpm exec electron-builder --mac --${{ matrix.arch }} --config electron-builder.yml -p never
- name: Verify native parser in packaged app (${{ matrix.arch }})
shell: bash
run: node scripts/check-desktop-native.mjs darwin-${{ matrix.arch }}
- name: Upload macOS artifacts (${{ matrix.arch }})
uses: actions/upload-artifact@v6
with:
name: ChatLab-mac-${{ matrix.arch }}
path: |
apps/desktop/dist/*.dmg
apps/desktop/dist/*.zip
apps/desktop/dist/*.yml
apps/desktop/dist/*.json
apps/desktop/dist/*.blockmap
if-no-files-found: warn
retention-days: 1 # 只保留1天,Release后会上传到GitHub Releases
build-win:
needs: [prepare_release]
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '24'
package-manager-cache: false
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
# 只暴露 System32 的 tar.exe 到 PATH。不能把整个 System32 置前:
# 那会让 WSL 的 bash.EXE 遮蔽 Git Bash,导致后续所有 shell: bash 步骤
# (含 dtolnay/rust-toolchain 内部脚本)在无 WSL 发行版的 runner 上失败
- name: Use Windows system tar (fix zstd cache issue)
shell: pwsh
run: |
$tarDir = Join-Path $env:RUNNER_TEMP 'system-tar'
New-Item -ItemType Directory -Force -Path $tarDir | Out-Null
Copy-Item "$env:SystemRoot\System32\tar.exe" -Destination $tarDir
Add-Content -Path $env:GITHUB_PATH -Value $tarDir
- name: Setup pnpm cache
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: |
echo "node-linker=hoisted" >> .npmrc
pnpm install --frozen-lockfile
# Rust native parser:Windows release 仅构建 x64(与 electron-builder --win 默认一致)
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: packages/parser-native
- name: Build native parser (win x64)
run: pnpm --filter @openchatlab/parser-native build
- name: Verify native parser artifact
shell: bash
run: ls -la packages/parser-native/chatlab-parser-native.win32-x64-msvc.node
- name: Inject version into app package.json
shell: pwsh
run: |
$env:RELEASE_VERSION = "${{ needs.prepare_release.outputs.version }}"
Write-Host "Injecting version: $env:RELEASE_VERSION"
node -e "
const fs = require('fs');
const version = process.env.RELEASE_VERSION;
for (const p of ['apps/desktop/package.json', 'apps/cli/package.json']) {
const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
pkg.version = version;
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
console.log(p + ' -> ' + version);
}
"
- name: Cache Electron and electron-builder binaries
uses: actions/cache@v5
with:
path: |
~/AppData/Local/electron/Cache
~/AppData/Local/electron-builder/Cache
key: ${{ runner.os }}-electron-builder-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-electron-builder-
- name: Build Electron app for Windows
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
# 分析服务
UMAMI_ENDPOINT: ${{ secrets.UMAMI_ENDPOINT }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID }}
working-directory: apps/desktop
run: pnpm run build:win
- name: Verify native parser in packaged app (win x64)
shell: bash
run: node scripts/check-desktop-native.mjs win32-x64-msvc
- name: Verify packaged SQLite runtime (win x64)
run: node scripts/check-packaged-sqlite.mjs
- name: Upload Windows artifacts
uses: actions/upload-artifact@v6
with:
name: ChatLab-win
path: |
apps/desktop/dist/*.exe
apps/desktop/dist/*.yml
apps/desktop/dist/*.json
apps/desktop/dist/*.blockmap
if-no-files-found: warn
retention-days: 1 # 只保留1天,Release后会上传到GitHub Releases
build-cli:
needs: [prepare_release]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '24'
package-manager-cache: false
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: |
echo "node-linker=hoisted" >> .npmrc
pnpm install --frozen-lockfile
- name: Inject CLI version
run: |
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('apps/cli/package.json', 'utf-8'));
pkg.version = '${{ needs.prepare_release.outputs.version }}';
fs.writeFileSync('apps/cli/package.json', JSON.stringify(pkg, null, 2) + '\n');
"
- name: Build CLI (with Web UI)
env:
UMAMI_ENDPOINT: ${{ secrets.UMAMI_ENDPOINT }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID }}
run: pnpm --filter chatlab-cli run build:full
- name: Upload CLI build output
uses: actions/upload-artifact@v6
with:
name: ChatLab-cli-build
path: |
apps/cli/dist
apps/cli/dist-cli-web
if-no-files-found: error
retention-days: 1
release:
needs: [prepare_release, build-mac, build-win, build-cli]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Download macOS artifacts (x64)
uses: actions/download-artifact@v8
with:
name: ChatLab-mac-x64
path: release-dist
- name: Download macOS artifacts (arm64)
uses: actions/download-artifact@v8
with:
name: ChatLab-mac-arm64
path: release-dist
- name: Download Windows artifacts
uses: actions/download-artifact@v8
with:
name: ChatLab-win
path: release-dist
- name: List files
run: ls -la release-dist/
- name: Generate Release Notes
id: release_notes
run: |
CURRENT_VERSION="${{ needs.prepare_release.outputs.version_tag }}"
VERSION_NUMBER="${{ needs.prepare_release.outputs.version }}"
echo "Current version: $CURRENT_VERSION"
# 按当前版本号从 changelog JSON 中提取 summary,避免补发旧版本时误用第一条日志。
EN_SUMMARY=$(jq -r --arg version "$VERSION_NUMBER" 'map(select(.version == $version))[0].summary // "See changelog for details"' changelogs/en.json 2>/dev/null || echo "See changelog for details")
CN_SUMMARY=$(jq -r --arg version "$VERSION_NUMBER" 'map(select(.version == $version))[0].summary // "详见更新日志"' changelogs/cn.json 2>/dev/null || echo "详见更新日志")
echo "EN summary: $EN_SUMMARY"
echo "CN summary: $CN_SUMMARY"
# 生成 release notes
{
echo "## What's New"
echo ""
echo "$EN_SUMMARY"
echo ""
echo "## 更新内容"
echo ""
echo "$CN_SUMMARY"
echo ""
echo "---"
echo ""
echo "Full Changelog: [English](https://github.com/ChatLab/ChatLab/blob/main/changelogs/en.md) | [中文](https://github.com/ChatLab/ChatLab/blob/main/changelogs/cn.md) | [日本語](https://github.com/ChatLab/ChatLab/blob/main/changelogs/ja.md)"
echo ""
echo "---"
echo ""
echo "## Download"
echo ""
echo "| Platform | File |"
echo "|-----------------|-------------|"
echo "| Mac (Apple Silicon) | [ChatLab-${VERSION_NUMBER}-arm64.dmg](https://github.com/ChatLab/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-arm64.dmg) |"
echo "| Mac (Intel) | [ChatLab-${VERSION_NUMBER}-x64.dmg](https://github.com/ChatLab/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-x64.dmg) |"
echo "| Windows | [ChatLab-${VERSION_NUMBER}-setup.exe](https://github.com/ChatLab/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-setup.exe) |"
} > release_notes.md
echo "Generated release notes:"
cat release_notes.md
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare_release.outputs.version_tag }}
name: ChatLab ${{ needs.prepare_release.outputs.version_tag }}
body_path: release_notes.md
files: |
release-dist/*.exe
release-dist/*.dmg
release-dist/*.zip
release-dist/*.yml
release-dist/*.blockmap
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 同步到 Cloudflare R2(国内镜像)
# 注意:beta/alpha/rc 等预发布版本不上传到 R2
- name: Print R2 upload metadata
run: |
echo "Release tag: ${{ needs.prepare_release.outputs.version_tag }}"
echo "Prerelease: ${{ needs.prepare_release.outputs.is_prerelease }}"
# 上传所有文件到版本目录
- name: Upload to Cloudflare R2 (version directory)
if: needs.prepare_release.outputs.is_prerelease == 'false'
uses: ryand56/r2-upload-action@latest
with:
r2-account-id: ${{ secrets.R2_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }}
r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
r2-bucket: ${{ secrets.R2_BUCKET_NAME }}
source-dir: release-dist
destination-dir: releases/download/${{ needs.prepare_release.outputs.version_tag }}
# 准备 yml 文件用于根目录上传
# 需要修改 yml 中的 path/url 为包含版本号的完整路径
- name: Prepare yml files for root upload
if: needs.prepare_release.outputs.is_prerelease == 'false'
run: |
mkdir -p dist-yml
VERSION_TAG="${{ needs.prepare_release.outputs.version_tag }}"
# 处理所有 yml 文件
for yml_file in release-dist/*.yml; do
filename=$(basename "$yml_file")
echo "Processing $filename..."
# 使用 sed 修改 path 和 url 字段,添加版本目录前缀
# path: ChatLab-x.x.x-arm64.dmg → path: vx.x.x/ChatLab-x.x.x-arm64.dmg
# url: ChatLab-x.x.x-arm64.dmg → url: vx.x.x/ChatLab-x.x.x-arm64.dmg
sed -E "s|^(path: )(.+)|\1${VERSION_TAG}/\2|g; s|^( - url: )(.+)|\1${VERSION_TAG}/\2|g" "$yml_file" > "dist-yml/$filename"
echo "Original:"
cat "$yml_file"
echo ""
echo "Modified:"
cat "dist-yml/$filename"
echo "---"
done
# 上传 yml 文件到根目录(覆盖旧版本,electron-updater 从这里检测更新)
- name: Upload yml files to Cloudflare R2 (root directory)
if: needs.prepare_release.outputs.is_prerelease == 'false'
uses: ryand56/r2-upload-action@latest
with:
r2-account-id: ${{ secrets.R2_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }}
r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
r2-bucket: ${{ secrets.R2_BUCKET_NAME }}
source-dir: dist-yml
destination-dir: releases/download
publish-cli:
needs: [prepare_release, build-cli]
runs-on: ubuntu-latest
# 必须显式声明 id-token 写入权限,用于 OIDC 与 npm 握手交换临时 Token
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
package-manager-cache: false
- name: Inject CLI version
run: |
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('apps/cli/package.json', 'utf-8'));
pkg.version = '${{ needs.prepare_release.outputs.version }}';
fs.writeFileSync('apps/cli/package.json', JSON.stringify(pkg, null, 2) + '\n');
"
- name: Download CLI build output
uses: actions/download-artifact@v8
with:
name: ChatLab-cli-build
path: apps/cli
- name: Copy README for npm publish
run: cp README.md apps/cli/README.md
- name: Publish to npm
working-directory: apps/cli
run: npm publish --ignore-scripts --provenance --tag ${{ needs.prepare_release.outputs.npm_tag }}
publish-docker:
needs: [prepare_release, release, publish-cli]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout image definition
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository_owner }}/chatlab-cli
tags: |
type=raw,value=${{ needs.prepare_release.outputs.version_tag }}
type=raw,value=latest,enable=${{ needs.prepare_release.outputs.npm_tag == 'latest' }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.version=${{ needs.prepare_release.outputs.version_tag }}
org.opencontainers.image.revision=${{ github.sha }}
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
file: .github/docker/chatlab-cli.Dockerfile
push: true
platforms: linux/amd64,linux/arm64
build-args: |
CHATLAB_VERSION=${{ needs.prepare_release.outputs.version }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=chatlab-cli
cache-to: type=gha,mode=max,scope=chatlab-cli
cleanup-r2:
needs: [prepare_release, release]
if: needs.prepare_release.outputs.is_prerelease == 'false'
uses: ./.github/workflows/cleanup-r2.yml
with:
keep_count: '3'
secrets:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}