name: Release on: push: tags: - 'v*' permissions: contents: write concurrency: group: release-${{ github.ref_name }} cancel-in-progress: true jobs: release: runs-on: ${{ matrix.os }} strategy: matrix: include: - os: windows-latest - os: ubuntu-latest - os: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Setup Rust uses: dtolnay/rust-toolchain@stable - name: Add macOS targets if: runner.os == 'macOS' run: | rustup target add aarch64-apple-darwin x86_64-apple-darwin - 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 # 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 uses: pnpm/action-setup@v2 with: version: 10.12.3 run_install: false - name: Get pnpm store directory id: pnpm-store shell: bash run: echo "path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Setup pnpm cache uses: actions/cache@v3 with: path: ${{ steps.pnpm-store.outputs.path }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: ${{ runner.os }}-pnpm-store- - name: Install frontend deps run: pnpm install --frozen-lockfile - name: Prepare Tauri signing key shell: bash run: | # 调试:检查 Secret 是否存在 if [ -z "${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}" ]; then echo "❌ TAURI_SIGNING_PRIVATE_KEY Secret 为空或不存在" >&2 echo "请检查 GitHub 仓库 Settings > Secrets and variables > Actions" >&2 exit 1 fi RAW="${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}" # 目标:提供正确的私钥“文件路径”给 Tauri CLI,避免内容解码歧义 KEY_PATH="$RUNNER_TEMP/tauri_signing.key" # 情况 1:原始两行文本(第一行以 "untrusted comment:" 开头) if echo "$RAW" | head -n1 | grep -q '^untrusted comment:'; then printf '%s\n' "$RAW" > "$KEY_PATH" echo "✅ 使用原始两行密钥文件格式" else # 情况 2:整体被 base64 包裹(解包后应当是两行) if DECODED=$(printf '%s' "$RAW" | (base64 --decode 2>/dev/null || base64 -D 2>/dev/null)) \ && echo "$DECODED" | head -n1 | grep -q '^untrusted comment:'; then printf '%s\n' "$DECODED" > "$KEY_PATH" echo "✅ 成功解码 base64 包裹密钥,已还原为两行文件" else # 情况 3:已是第二行(纯 Base64 一行)→ 构造两行文件 if echo "$RAW" | grep -Eq '^[A-Za-z0-9+/=]+$'; then ONE=$(printf '%s' "$RAW" | tr -d '\r\n') printf '%s\n%s\n' "untrusted comment: tauri signing key" "$ONE" > "$KEY_PATH" echo "✅ 使用一行 Base64 私钥,已构造两行文件" else echo "❌ TAURI_SIGNING_PRIVATE_KEY 格式无法识别:既不是两行原文,也不是其 base64,亦非一行 base64" >&2 echo "密钥前10个字符: $(echo "$RAW" | head -c 10)..." >&2 exit 1 fi fi fi # 将“完整两行内容”作为环境变量注入(Tauri 支持传入完整私钥文本或文件路径) # 使用多行写入语法,保持换行以便解析 # 将完整两行私钥内容进行 base64 编码,作为单行内容注入环境变量 if command -v base64 >/dev/null 2>&1; then KEY_B64=$(base64 < "$KEY_PATH" | tr -d '\r\n') elif command -v openssl >/dev/null 2>&1; then KEY_B64=$(openssl base64 -A -in "$KEY_PATH") else KEY_B64=$(KEY_PATH="$KEY_PATH" node -e "process.stdout.write(require('fs').readFileSync(process.env.KEY_PATH).toString('base64'))") fi if [ -z "$KEY_B64" ]; then echo "❌ 无法生成私钥 base64 内容" >&2 exit 1 fi echo "TAURI_SIGNING_PRIVATE_KEY=$KEY_B64" >> "$GITHUB_ENV" if [ -n "${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}" ]; then echo "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}" >> $GITHUB_ENV fi echo "✅ Tauri signing key prepared" - name: Build Tauri App (macOS) if: runner.os == 'macOS' run: pnpm tauri build --target universal-apple-darwin - name: Build Tauri App (Windows) if: runner.os == 'Windows' run: pnpm tauri build - name: Build Tauri App (Linux) if: runner.os == 'Linux' run: pnpm tauri build - name: Prepare macOS Assets if: runner.os == 'macOS' shell: bash run: | set -euxo pipefail mkdir -p release-assets echo "Looking for .app bundle..." APP_PATH="" for path in \ "src-tauri/target/release/bundle/macos" \ "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"; do if [ -d "$path" ]; then APP_PATH=$(find "$path" -name "*.app" -type d | head -1) [ -n "$APP_PATH" ] && break fi done if [ -z "$APP_PATH" ]; then echo "No .app found" >&2 exit 1 fi APP_DIR=$(dirname "$APP_PATH") APP_NAME=$(basename "$APP_PATH") cd "$APP_DIR" # 使用 ditto 打包更兼容资源分叉 ditto -c -k --sequesterRsrc --keepParent "$APP_NAME" "CC-Switch-macOS.zip" mv "CC-Switch-macOS.zip" "$GITHUB_WORKSPACE/release-assets/" echo "macOS zip ready" - name: Prepare Windows Assets if: runner.os == 'Windows' shell: pwsh run: | $ErrorActionPreference = 'Stop' New-Item -ItemType Directory -Force -Path release-assets | Out-Null # 安装器(优先 NSIS,其次 MSI) $installer = Get-ChildItem -Path 'src-tauri/target/release/bundle' -Recurse -Include *.exe,*.msi -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match '\\bundle\\(nsis|msi)\\' } | Select-Object -First 1 if ($null -ne $installer) { $dest = if ($installer.Extension -ieq '.msi') { 'CC-Switch-Setup.msi' } else { 'CC-Switch-Setup.exe' } Copy-Item $installer.FullName (Join-Path release-assets $dest) Write-Host "Installer copied: $dest" } else { Write-Warning 'No Windows installer found' } # 绿色版(portable):仅可执行文件 $exeCandidates = @( 'src-tauri/target/release/cc-switch.exe', 'src-tauri/target/x86_64-pc-windows-msvc/release/cc-switch.exe' ) $exePath = $exeCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 if ($null -ne $exePath) { $portableDir = 'release-assets/CC-Switch-Portable' New-Item -ItemType Directory -Force -Path $portableDir | Out-Null Copy-Item $exePath $portableDir Compress-Archive -Path "$portableDir/*" -DestinationPath 'release-assets/CC-Switch-Windows-Portable.zip' -Force Remove-Item -Recurse -Force $portableDir Write-Host 'Windows portable zip created' } else { Write-Warning 'Portable exe not found' } - name: Prepare Linux Assets if: runner.os == 'Linux' shell: bash run: | set -euxo pipefail mkdir -p release-assets # 仅上传安装包(deb) DEB=$(find src-tauri/target/release/bundle -name "*.deb" | head -1 || true) if [ -n "$DEB" ]; then cp "$DEB" release-assets/ echo "Deb package copied" else echo "No .deb found" >&2 exit 1 fi - name: List prepared assets shell: bash run: | ls -la release-assets || true - name: Collect Signatures shell: bash run: | # 查找并复制签名文件到 release-assets find src-tauri/target -name "*.sig" -type f 2>/dev/null | while read sig; do cp "$sig" release-assets/ || true done echo "Collected signatures:" ls -la release-assets/*.sig || echo "No signatures found" # 查找并复制 latest.json(updater manifest)到 release-assets FOUND_JSON=false while IFS= read -r json; do cp "$json" release-assets/ || true echo "Copied updater manifest: $json" FOUND_JSON=true done < <(find src-tauri/target -name "latest.json" -type f 2>/dev/null) if [ "$FOUND_JSON" = false ]; then echo "Warning: latest.json not found under src-tauri/target" >&2 fi - name: Upload Release Assets uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: CC Switch ${{ github.ref_name }} prerelease: true body: | ## CC Switch ${{ github.ref_name }} Claude Code 供应商切换工具 ### 下载 - macOS: `CC-Switch-macOS.zip`(解压即用) - Windows: `CC-Switch-Setup.exe` 或 `CC-Switch-Setup.msi`(安装版);`CC-Switch-Windows-Portable.zip`(绿色版) - Linux: `*.deb`(Debian/Ubuntu 安装包) --- 提示:macOS 如遇“已损坏”提示,可在终端执行:`xattr -cr "/Applications/CC Switch.app"` files: release-assets/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - 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