feat: sync latest workspace changes #289
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| paths-ignore: | |
| - "docs/**" | |
| - "*.md" | |
| - ".github/workflows/deploy-docs.yml" | |
| - ".github/workflows/update-homebrew.yml" | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version to release (e.g., v0.1.0)" | |
| required: true | |
| default: "v0.1.0" | |
| source_ref: | |
| description: "Git ref to build from (e.g., v0.96.0 or main)" | |
| required: false | |
| default: "" | |
| permissions: | |
| contents: write | |
| packages: write | |
| actions: read | |
| env: | |
| CARGO_NET_RETRY: 10 | |
| RUSTUP_MAX_RETRIES: 10 | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| prepare_release: | |
| name: Prepare GitHub release | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event.inputs.source_ref || github.ref }} | |
| - name: Ensure GitHub release exists | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ github.event.inputs.version || github.ref_name }}" | |
| TARGET_REF="${{ github.event.inputs.source_ref || github.sha }}" | |
| NOTES_FILE="$RUNNER_TEMP/release-notes.md" | |
| if [[ "$TAG" == *-* ]]; then | |
| IS_PRERELEASE="true" | |
| else | |
| IS_PRERELEASE="false" | |
| fi | |
| if [ -f RELEASE_NOTES.md ]; then | |
| cp RELEASE_NOTES.md "$NOTES_FILE" | |
| else | |
| PREV_TAG="$(git tag --sort=-v:refname | grep -v "^${TAG}$" | head -n 1)" | |
| if [ -z "$PREV_TAG" ]; then | |
| CHANGELOG="$(git log --pretty=format:"- %s (%h)" "${TAG}" 2>/dev/null || git log --pretty=format:"- %s (%h)")" | |
| else | |
| CHANGELOG="$(git log --pretty=format:"- %s (%h)" "${PREV_TAG}..${TAG}" 2>/dev/null || git log --pretty=format:"- %s (%h)" "${PREV_TAG}..HEAD")" | |
| fi | |
| { | |
| echo "## Lime ${TAG}" | |
| echo | |
| echo "### Changes" | |
| echo "${CHANGELOG}" | |
| } > "$NOTES_FILE" | |
| fi | |
| if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| echo "Release $TAG already exists" | |
| exit 0 | |
| fi | |
| create_args=( | |
| release create "$TAG" | |
| --repo "$GITHUB_REPOSITORY" | |
| --target "$TARGET_REF" | |
| --title "Lime $TAG" | |
| --notes-file "$NOTES_FILE" | |
| ) | |
| if [ "$IS_PRERELEASE" = "true" ]; then | |
| create_args+=(--prerelease) | |
| fi | |
| gh "${create_args[@]}" || gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null | |
| build: | |
| needs: prepare_release | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: macos-latest | |
| target: aarch64-apple-darwin | |
| name: macOS-arm64 | |
| build_cli: true | |
| - platform: macos-15-intel | |
| target: x86_64-apple-darwin | |
| name: macOS-x64 | |
| build_cli: true | |
| - platform: windows-2022 | |
| target: x86_64-pc-windows-msvc | |
| name: Windows-x64 | |
| build_cli: true | |
| - platform: ubuntu-22.04 | |
| target: x86_64-unknown-linux-gnu | |
| name: Linux-x64-cli | |
| build_cli: true | |
| runs-on: ${{ matrix.platform }} | |
| env: | |
| LIME_UPDATER_PUBLIC_KEY: ${{ secrets.LIME_UPDATER_PUBLIC_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_RAW: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| NODE_OPTIONS: --max-old-space-size=8192 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event.inputs.source_ref || github.ref }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: "pnpm" | |
| cache-dependency-path: "pnpm-lock.yaml" | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Setup sccache | |
| uses: mozilla-actions/sccache-action@v0.0.9 | |
| - name: Setup Rust cache | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: src-tauri | |
| shared-key: "rust-${{ matrix.target }}" | |
| cache-on-failure: true | |
| cache-all-crates: true | |
| save-if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }} | |
| - name: Resolve updater release mode | |
| id: updater_mode | |
| shell: bash | |
| run: | | |
| if [ -n "${LIME_UPDATER_PUBLIC_KEY}" ] && [ -n "${TAURI_SIGNING_PRIVATE_KEY_RAW}" ] && [ -n "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD}" ]; then | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| echo "LIME_ENABLE_UPDATER_ARTIFACTS=true" >> "$GITHUB_ENV" | |
| echo "Updater artifacts enabled" | |
| else | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "LIME_ENABLE_UPDATER_ARTIFACTS=false" >> "$GITHUB_ENV" | |
| echo "LIME_UPDATER_PUBLIC_KEY=" >> "$GITHUB_ENV" | |
| echo "TAURI_SIGNING_PRIVATE_KEY_RAW=" >> "$GITHUB_ENV" | |
| echo "TAURI_SIGNING_PRIVATE_KEY=" >> "$GITHUB_ENV" | |
| echo "TAURI_SIGNING_PRIVATE_KEY_PATH=" >> "$GITHUB_ENV" | |
| echo "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=" >> "$GITHUB_ENV" | |
| echo "::warning::Updater signing secrets are missing. Release will skip updater artifacts and fall back to manual-download updates." | |
| fi | |
| - name: Sync version from tag to tauri configs | |
| shell: bash | |
| run: | | |
| VERSION="${{ github.event.inputs.version || github.ref_name }}" | |
| VERSION="${VERSION#v}" | |
| echo "Syncing version to $VERSION" | |
| cd src-tauri | |
| node -e " | |
| const fs = require('fs'); | |
| for (const file of ['tauri.conf.json', 'tauri.conf.headless.json']) { | |
| const conf = JSON.parse(fs.readFileSync(file, 'utf8')); | |
| conf.version = '$VERSION'; | |
| conf.bundle = conf.bundle || {}; | |
| conf.bundle.createUpdaterArtifacts = process.env.LIME_ENABLE_UPDATER_ARTIFACTS === 'true'; | |
| fs.writeFileSync(file, JSON.stringify(conf, null, 2) + '\n'); | |
| } | |
| " | |
| echo "tauri.conf.json version is now: $(node -e "console.log(JSON.parse(require('fs').readFileSync('tauri.conf.json','utf8')).version)")" | |
| echo "tauri.conf.headless.json version is now: $(node -e "console.log(JSON.parse(require('fs').readFileSync('tauri.conf.headless.json','utf8')).version)")" | |
| echo "createUpdaterArtifacts: $(node -e "console.log(JSON.parse(require('fs').readFileSync('tauri.conf.json','utf8')).bundle.createUpdaterArtifacts)")" | |
| - name: Resolve release channel | |
| id: release_meta | |
| shell: bash | |
| run: | | |
| TAG="${{ github.event.inputs.version || github.ref_name }}" | |
| if [[ "$TAG" == *-* ]]; then | |
| echo "is_prerelease=true" >> "$GITHUB_OUTPUT" | |
| echo "Release channel: prerelease" | |
| else | |
| echo "is_prerelease=false" >> "$GITHUB_OUTPUT" | |
| echo "Release channel: stable" | |
| fi | |
| - name: Load release notes | |
| shell: bash | |
| run: | | |
| if [ -f RELEASE_NOTES.md ]; then | |
| { | |
| echo "RELEASE_BODY<<RELEASE_BODY_EOF" | |
| cat RELEASE_NOTES.md | |
| echo "" | |
| echo "RELEASE_BODY_EOF" | |
| } >> "$GITHUB_ENV" | |
| else | |
| # fallback: 从 git log 生成简单 changelog | |
| TAG="${{ github.event.inputs.version || github.ref_name }}" | |
| PREV_TAG=$(git tag --sort=-v:refname | grep -v "^${TAG}$" | head -n 1) | |
| if [ -z "$PREV_TAG" ]; then | |
| CHANGELOG=$(git log --pretty=format:"- %s (%h)" ${TAG} 2>/dev/null || git log --pretty=format:"- %s (%h)") | |
| else | |
| CHANGELOG=$(git log --pretty=format:"- %s (%h)" ${PREV_TAG}..${TAG} 2>/dev/null || git log --pretty=format:"- %s (%h)" ${PREV_TAG}..HEAD) | |
| fi | |
| { | |
| echo "RELEASE_BODY<<RELEASE_BODY_EOF" | |
| echo "## Lime ${TAG}" | |
| echo "" | |
| echo "### Changes" | |
| echo "${CHANGELOG}" | |
| echo "RELEASE_BODY_EOF" | |
| } >> "$GITHUB_ENV" | |
| fi | |
| - name: Install frontend dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Ensure Tauri CLI native binding | |
| if: startsWith(matrix.platform, 'macos') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if node -e "require('@tauri-apps/cli')"; then | |
| echo "Tauri CLI native binding is ready" | |
| exit 0 | |
| fi | |
| echo "::warning::Tauri CLI native binding missing after initial install; retrying clean install." | |
| rm -rf node_modules | |
| pnpm install --frozen-lockfile --force | |
| node -e "require('@tauri-apps/cli')" | |
| - name: Normalize updater signing key | |
| if: steps.updater_mode.outputs.enabled == 'true' | |
| shell: bash | |
| run: | | |
| python - <<'PY' | |
| import base64 | |
| import os | |
| from pathlib import Path | |
| raw = os.environ.get("TAURI_SIGNING_PRIVATE_KEY_RAW", "") | |
| if not raw.strip(): | |
| raise SystemExit("TAURI_SIGNING_PRIVATE_KEY secret is empty") | |
| candidates = [] | |
| def add(value: str) -> None: | |
| normalized = value.replace("\r\n", "\n") | |
| if normalized and normalized not in candidates: | |
| candidates.append(normalized) | |
| add(raw) | |
| if "\\n" in raw and "\n" not in raw: | |
| add(raw.replace("\\r\\n", "\n").replace("\\n", "\n")) | |
| for candidate in list(candidates): | |
| compact = candidate.strip() | |
| if not compact: | |
| continue | |
| padding = (-len(compact)) % 4 | |
| compact = compact + ("=" * padding) | |
| try: | |
| decoded = base64.b64decode(compact, validate=True).decode("utf-8") | |
| except Exception: | |
| continue | |
| add(decoded) | |
| if "\\n" in decoded and "\n" not in decoded: | |
| add(decoded.replace("\\r\\n", "\n").replace("\\n", "\n")) | |
| normalized_key = next( | |
| ( | |
| candidate | |
| for candidate in candidates | |
| if candidate.lstrip().startswith("untrusted comment:") | |
| and "\n" in candidate | |
| ), | |
| "", | |
| ) | |
| if not normalized_key: | |
| normalized_key = next( | |
| ( | |
| candidate | |
| for candidate in candidates | |
| if candidate.lstrip().startswith("untrusted comment:") | |
| ), | |
| "", | |
| ) | |
| if not normalized_key: | |
| raise SystemExit( | |
| "Unable to normalize TAURI_SIGNING_PRIVATE_KEY into minisign secret key content. " | |
| "Expected raw multiline key, literal \\\\n escaped key, or base64 encoded key." | |
| ) | |
| key_path = Path(os.environ["RUNNER_TEMP"]) / "tauri-updater.key" | |
| key_path.write_text(normalized_key.rstrip("\n") + "\n", encoding="utf-8") | |
| key_path.chmod(0o600) | |
| with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env_file: | |
| env_file.write(f"TAURI_SIGNING_PRIVATE_KEY_PATH={key_path}\n") | |
| env_file.write("TAURI_SIGNING_PRIVATE_KEY<<__TAURI_SIGNING_PRIVATE_KEY__\n") | |
| env_file.write(normalized_key.rstrip("\n") + "\n") | |
| env_file.write("__TAURI_SIGNING_PRIVATE_KEY__\n") | |
| print(f"Normalized updater key written to {key_path}") | |
| PY | |
| - name: Validate updater signing key | |
| if: steps.updater_mode.outputs.enabled == 'true' | |
| shell: bash | |
| run: | | |
| PROBE_FILE="$RUNNER_TEMP/tauri-signing-probe.txt" | |
| printf 'lime-release-signing-probe\n' > "$PROBE_FILE" | |
| npx tauri signer sign \ | |
| -f "$TAURI_SIGNING_PRIVATE_KEY_PATH" \ | |
| -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \ | |
| "$PROBE_FILE" >/dev/null | |
| # macOS 签名证书设置 | |
| - name: Import Apple Certificate | |
| if: startsWith(matrix.platform, 'macos') | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| # 创建临时 keychain | |
| KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db | |
| KEYCHAIN_PASSWORD=${{ secrets.KEYCHAIN_PASSWORD }} | |
| # 解码证书 | |
| echo -n "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 | |
| # 创建 keychain | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| security set-keychain-settings -lut 21600 $KEYCHAIN_PATH | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| # 导入证书 | |
| security import $RUNNER_TEMP/certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH | |
| security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| security list-keychain -d user -s $KEYCHAIN_PATH | |
| - name: Validate macOS notarization prerequisites | |
| if: startsWith(matrix.platform, 'macos') | |
| shell: bash | |
| env: | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| missing=() | |
| [ -n "${APPLE_SIGNING_IDENTITY:-}" ] || missing+=("APPLE_SIGNING_IDENTITY") | |
| [ -n "${APPLE_ID:-}" ] || missing+=("APPLE_ID") | |
| [ -n "${APPLE_PASSWORD:-}" ] || missing+=("APPLE_PASSWORD") | |
| [ -n "${APPLE_TEAM_ID:-}" ] || missing+=("APPLE_TEAM_ID") | |
| if [ "${#missing[@]}" -gt 0 ]; then | |
| printf 'Missing macOS notarization secrets: %s\n' "${missing[*]}" >&2 | |
| exit 1 | |
| fi | |
| - name: Build Tauri app (Windows offline, direct release upload) | |
| if: matrix.platform == 'windows-2022' && steps.updater_mode.outputs.enabled == 'true' | |
| uses: tauri-apps/tauri-action@v0 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # 禁用 LTO 加速编译(正式发布可改为 thin) | |
| CARGO_PROFILE_RELEASE_LTO: "off" | |
| # 增加并行编译单元 | |
| CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32 | |
| CARGO_INCREMENTAL: 0 | |
| SCCACHE_GHA_ENABLED: "true" | |
| RUSTC_WRAPPER: sccache | |
| with: | |
| tauriScript: npx tauri | |
| projectPath: src-tauri | |
| tagName: ${{ github.event.inputs.version || github.ref_name }} | |
| releaseName: "Lime ${{ github.event.inputs.version || github.ref_name }}" | |
| releaseBody: ${{ env.RELEASE_BODY }} | |
| releaseDraft: false | |
| prerelease: ${{ steps.release_meta.outputs.is_prerelease == 'true' }} | |
| includeUpdaterJson: ${{ steps.updater_mode.outputs.enabled == 'true' }} | |
| # tauri-action@v0 使用 assetNamePattern,离线包优先上传并保留独立名称 | |
| assetNamePattern: "[name]_[version]_[arch]-offline[setup][ext]" | |
| # 默认不启用 voice feature(包含 whisper-rs,编译很慢) | |
| args: --target ${{ matrix.target }} --config tauri.windows.conf.json | |
| - name: Build Tauri app (Windows offline, stage only) | |
| id: build_windows_offline_stage | |
| if: matrix.platform == 'windows-2022' && steps.updater_mode.outputs.enabled == 'false' | |
| uses: tauri-apps/tauri-action@v0 | |
| env: | |
| # 禁用 LTO 加速编译(正式发布可改为 thin) | |
| CARGO_PROFILE_RELEASE_LTO: "off" | |
| # 增加并行编译单元 | |
| CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32 | |
| CARGO_INCREMENTAL: 0 | |
| SCCACHE_GHA_ENABLED: "true" | |
| RUSTC_WRAPPER: sccache | |
| with: | |
| tauriScript: npx tauri | |
| projectPath: src-tauri | |
| # 默认不启用 voice feature(包含 whisper-rs,编译很慢) | |
| args: --target ${{ matrix.target }} --config tauri.windows.conf.json | |
| - name: Stage Windows offline release asset | |
| if: matrix.platform == 'windows-2022' && steps.updater_mode.outputs.enabled == 'false' | |
| shell: bash | |
| env: | |
| ARTIFACT_PATHS_JSON: ${{ steps.build_windows_offline_stage.outputs.artifactPaths }} | |
| RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }} | |
| TARGET_TRIPLE: ${{ matrix.target }} | |
| STAGING_DIR: release-assets/${{ matrix.target }} | |
| run: | | |
| set -euo pipefail | |
| node - <<'NODE' | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const raw = (process.env.ARTIFACT_PATHS_JSON || '').trim(); | |
| if (!raw) { | |
| throw new Error('Windows offline artifactPaths 输出为空'); | |
| } | |
| let artifacts; | |
| try { | |
| artifacts = JSON.parse(raw); | |
| } catch (error) { | |
| throw new Error(`Windows offline artifactPaths 不是合法 JSON: ${raw}\n${error}`); | |
| } | |
| if (!Array.isArray(artifacts) || artifacts.length === 0) { | |
| throw new Error(`Windows offline artifactPaths 为空数组: ${raw}`); | |
| } | |
| const installerPath = artifacts.find( | |
| (item) => typeof item === 'string' && item.toLowerCase().endsWith('.exe') | |
| ); | |
| if (!installerPath) { | |
| throw new Error(`Windows offline 构建未找到 .exe 安装包: ${raw}`); | |
| } | |
| const version = (process.env.RELEASE_TAG || '').replace(/^v/, ''); | |
| if (!version) { | |
| throw new Error('RELEASE_TAG 为空,无法生成 Windows release 文件名'); | |
| } | |
| const targetTriple = process.env.TARGET_TRIPLE || ''; | |
| const archLabel = | |
| targetTriple.startsWith('x86_64-') ? 'x64' : | |
| targetTriple.startsWith('aarch64-') ? 'aarch64' : | |
| targetTriple.replace(/[^A-Za-z0-9._-]+/g, '-'); | |
| const stagingDir = process.env.STAGING_DIR; | |
| fs.mkdirSync(stagingDir, { recursive: true }); | |
| const stagedPath = path.join( | |
| stagingDir, | |
| `Lime_${version}_${archLabel}-offline-setup.exe` | |
| ); | |
| fs.copyFileSync(installerPath, stagedPath); | |
| console.log(`Staged Windows offline installer: ${installerPath} -> ${stagedPath}`); | |
| NODE | |
| - name: Build Tauri app (Windows online, direct release upload) | |
| if: matrix.platform == 'windows-2022' && steps.updater_mode.outputs.enabled == 'true' | |
| uses: tauri-apps/tauri-action@v0 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # 禁用 LTO 加速编译(正式发布可改为 thin) | |
| CARGO_PROFILE_RELEASE_LTO: "off" | |
| # 增加并行编译单元 | |
| CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32 | |
| CARGO_INCREMENTAL: 0 | |
| SCCACHE_GHA_ENABLED: "true" | |
| RUSTC_WRAPPER: sccache | |
| with: | |
| tauriScript: npx tauri | |
| projectPath: src-tauri | |
| tagName: ${{ github.event.inputs.version || github.ref_name }} | |
| releaseName: "Lime ${{ github.event.inputs.version || github.ref_name }}" | |
| releaseBody: ${{ env.RELEASE_BODY }} | |
| releaseDraft: false | |
| prerelease: ${{ steps.release_meta.outputs.is_prerelease == 'true' }} | |
| includeUpdaterJson: ${{ steps.updater_mode.outputs.enabled == 'true' }} | |
| # tauri-action@v0 使用 assetNamePattern,在线包作为体积更小的备选 | |
| assetNamePattern: "[name]_[version]_[arch]-online[setup][ext]" | |
| # 默认不启用 voice feature(包含 whisper-rs,编译很慢) | |
| args: --target ${{ matrix.target }} --config tauri.windows.online.conf.json | |
| - name: Build Tauri app (Windows online, stage only) | |
| id: build_windows_online_stage | |
| if: matrix.platform == 'windows-2022' && steps.updater_mode.outputs.enabled == 'false' | |
| uses: tauri-apps/tauri-action@v0 | |
| env: | |
| # 禁用 LTO 加速编译(正式发布可改为 thin) | |
| CARGO_PROFILE_RELEASE_LTO: "off" | |
| # 增加并行编译单元 | |
| CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32 | |
| CARGO_INCREMENTAL: 0 | |
| SCCACHE_GHA_ENABLED: "true" | |
| RUSTC_WRAPPER: sccache | |
| with: | |
| tauriScript: npx tauri | |
| projectPath: src-tauri | |
| # 默认不启用 voice feature(包含 whisper-rs,编译很慢) | |
| args: --target ${{ matrix.target }} --config tauri.windows.online.conf.json | |
| - name: Stage Windows online release asset | |
| if: matrix.platform == 'windows-2022' && steps.updater_mode.outputs.enabled == 'false' | |
| shell: bash | |
| env: | |
| ARTIFACT_PATHS_JSON: ${{ steps.build_windows_online_stage.outputs.artifactPaths }} | |
| RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }} | |
| TARGET_TRIPLE: ${{ matrix.target }} | |
| STAGING_DIR: release-assets/${{ matrix.target }} | |
| run: | | |
| set -euo pipefail | |
| node - <<'NODE' | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const raw = (process.env.ARTIFACT_PATHS_JSON || '').trim(); | |
| if (!raw) { | |
| throw new Error('Windows online artifactPaths 输出为空'); | |
| } | |
| let artifacts; | |
| try { | |
| artifacts = JSON.parse(raw); | |
| } catch (error) { | |
| throw new Error(`Windows online artifactPaths 不是合法 JSON: ${raw}\n${error}`); | |
| } | |
| if (!Array.isArray(artifacts) || artifacts.length === 0) { | |
| throw new Error(`Windows online artifactPaths 为空数组: ${raw}`); | |
| } | |
| const installerPath = artifacts.find( | |
| (item) => typeof item === 'string' && item.toLowerCase().endsWith('.exe') | |
| ); | |
| if (!installerPath) { | |
| throw new Error(`Windows online 构建未找到 .exe 安装包: ${raw}`); | |
| } | |
| const version = (process.env.RELEASE_TAG || '').replace(/^v/, ''); | |
| if (!version) { | |
| throw new Error('RELEASE_TAG 为空,无法生成 Windows release 文件名'); | |
| } | |
| const targetTriple = process.env.TARGET_TRIPLE || ''; | |
| const archLabel = | |
| targetTriple.startsWith('x86_64-') ? 'x64' : | |
| targetTriple.startsWith('aarch64-') ? 'aarch64' : | |
| targetTriple.replace(/[^A-Za-z0-9._-]+/g, '-'); | |
| const stagingDir = process.env.STAGING_DIR; | |
| fs.mkdirSync(stagingDir, { recursive: true }); | |
| const stagedPath = path.join( | |
| stagingDir, | |
| `Lime_${version}_${archLabel}-online-setup.exe` | |
| ); | |
| fs.copyFileSync(installerPath, stagedPath); | |
| console.log(`Staged Windows online installer: ${installerPath} -> ${stagedPath}`); | |
| NODE | |
| - name: Build macOS app (attempt 1, verbose) | |
| id: build_macos_primary | |
| if: startsWith(matrix.platform, 'macos') | |
| continue-on-error: true | |
| shell: bash | |
| env: | |
| # 禁用 LTO 加速编译(正式发布可改为 thin) | |
| CARGO_PROFILE_RELEASE_LTO: "off" | |
| # 增加并行编译单元 | |
| CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32 | |
| CARGO_INCREMENTAL: 0 | |
| CARGO_TARGET_DIR: src-tauri/target | |
| SCCACHE_GHA_ENABLED: "true" | |
| RUSTC_WRAPPER: sccache | |
| # Apple 签名环境变量 | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| # Apple 公证环境变量(解决 Gatekeeper 警告) | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euxo pipefail | |
| echo "macOS target: ${{ matrix.target }}" | |
| echo "bundle candidates:" | |
| echo " - src-tauri/target/${{ matrix.target }}/release/bundle" | |
| echo " - src-tauri/src-tauri/target/${{ matrix.target }}/release/bundle" | |
| echo " - target/${{ matrix.target }}/release/bundle" | |
| pnpm tauri build --target "${{ matrix.target }}" | |
| - name: Inspect macOS outputs after attempt 1 | |
| if: startsWith(matrix.platform, 'macos') && always() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TARGET_TRIPLE="${{ matrix.target }}" | |
| bundle_dirs=() | |
| while IFS= read -r bundle_dir; do | |
| [ -n "$bundle_dir" ] || continue | |
| bundle_dirs+=("$bundle_dir") | |
| done < <(find . -type d -path "*/target/${TARGET_TRIPLE}/release/bundle" | sort) | |
| if [ "${#bundle_dirs[@]}" -eq 0 ]; then | |
| echo "Bundle dir not created yet for ${TARGET_TRIPLE}" | |
| exit 0 | |
| fi | |
| printf 'Detected macOS bundle dirs after attempt 1:\n' | |
| printf ' - %s\n' "${bundle_dirs[@]}" | |
| for bundle_dir in "${bundle_dirs[@]}"; do | |
| find "$bundle_dir" -maxdepth 4 -type f | sort | |
| done | |
| - name: Build macOS app (retry, verbose) | |
| id: build_macos_retry | |
| if: startsWith(matrix.platform, 'macos') && steps.build_macos_primary.outcome == 'failure' | |
| continue-on-error: true | |
| shell: bash | |
| env: | |
| CARGO_PROFILE_RELEASE_LTO: "off" | |
| CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32 | |
| CARGO_INCREMENTAL: 0 | |
| CARGO_TARGET_DIR: src-tauri/target | |
| SCCACHE_GHA_ENABLED: "true" | |
| RUSTC_WRAPPER: sccache | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euxo pipefail | |
| echo "Retry macOS target: ${{ matrix.target }}" | |
| pnpm tauri build --target "${{ matrix.target }}" | |
| - name: Inspect macOS outputs after retry | |
| if: startsWith(matrix.platform, 'macos') && steps.build_macos_primary.outcome == 'failure' && always() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TARGET_TRIPLE="${{ matrix.target }}" | |
| bundle_dirs=() | |
| while IFS= read -r bundle_dir; do | |
| [ -n "$bundle_dir" ] || continue | |
| bundle_dirs+=("$bundle_dir") | |
| done < <(find . -type d -path "*/target/${TARGET_TRIPLE}/release/bundle" | sort) | |
| if [ "${#bundle_dirs[@]}" -eq 0 ]; then | |
| echo "Bundle dir not created yet for ${TARGET_TRIPLE}" | |
| exit 0 | |
| fi | |
| printf 'Detected macOS bundle dirs after retry:\n' | |
| printf ' - %s\n' "${bundle_dirs[@]}" | |
| for bundle_dir in "${bundle_dirs[@]}"; do | |
| find "$bundle_dir" -maxdepth 4 -type f | sort | |
| done | |
| - name: Fail when macOS notarized build is unavailable | |
| if: startsWith(matrix.platform, 'macos') && steps.build_macos_primary.outcome == 'failure' && steps.build_macos_retry.outcome == 'failure' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "::error::macOS notarization failed twice. Blocking release instead of publishing a signed-only artifact." | |
| exit 1 | |
| - name: Resolve macOS bundle dir | |
| id: resolve_macos_bundle_dir | |
| if: startsWith(matrix.platform, 'macos') && (steps.build_macos_primary.outcome == 'success' || steps.build_macos_retry.outcome == 'success') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TARGET_TRIPLE="${{ matrix.target }}" | |
| candidates=( | |
| "src-tauri/target/${TARGET_TRIPLE}/release/bundle" | |
| "src-tauri/src-tauri/target/${TARGET_TRIPLE}/release/bundle" | |
| "target/${TARGET_TRIPLE}/release/bundle" | |
| ) | |
| bundle_dir="" | |
| for candidate in "${candidates[@]}"; do | |
| if [ -d "$candidate" ]; then | |
| bundle_dir="$candidate" | |
| break | |
| fi | |
| done | |
| if [ -z "$bundle_dir" ]; then | |
| bundle_dir="$(find . -type d -path "*/target/${TARGET_TRIPLE}/release/bundle" | sort | head -n 1)" | |
| fi | |
| if [ -z "$bundle_dir" ]; then | |
| echo "Unable to resolve macOS bundle dir for ${TARGET_TRIPLE}" >&2 | |
| find . -type d -path "*/target/${TARGET_TRIPLE}/release*" | sort || true | |
| exit 1 | |
| fi | |
| echo "Resolved macOS bundle dir: $bundle_dir" | |
| echo "bundle_dir=$bundle_dir" >> "$GITHUB_OUTPUT" | |
| - name: Verify notarized macOS app bundle | |
| if: startsWith(matrix.platform, 'macos') && (steps.build_macos_primary.outcome == 'success' || steps.build_macos_retry.outcome == 'success') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| BUNDLE_DIR="${{ steps.resolve_macos_bundle_dir.outputs.bundle_dir }}" | |
| if [ -z "$BUNDLE_DIR" ] || [ ! -d "$BUNDLE_DIR" ]; then | |
| echo "Expected bundle dir missing: $BUNDLE_DIR" >&2 | |
| exit 1 | |
| fi | |
| app_bundle="$(find "$BUNDLE_DIR" -type d -name "*.app" | sort | head -n 1)" | |
| if [ -z "$app_bundle" ] || [ ! -d "$app_bundle" ]; then | |
| echo "No .app bundle found under $BUNDLE_DIR" >&2 | |
| find "$BUNDLE_DIR" -maxdepth 4 | sort || true | |
| exit 1 | |
| fi | |
| echo "Validating notarized app bundle: $app_bundle" | |
| spctl -a -vv "$app_bundle" | |
| xcrun stapler validate "$app_bundle" | |
| - name: Inspect final macOS outputs | |
| if: startsWith(matrix.platform, 'macos') && always() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| BUNDLE_DIR="${{ steps.resolve_macos_bundle_dir.outputs.bundle_dir }}" | |
| if [ -z "$BUNDLE_DIR" ]; then | |
| echo "Bundle dir missing for ${{ matrix.target }}" | |
| exit 0 | |
| fi | |
| if [ -d "$BUNDLE_DIR" ]; then | |
| echo "Resolved bundle dir: $BUNDLE_DIR" | |
| find "$BUNDLE_DIR" -maxdepth 4 -type f | sort | |
| else | |
| echo "Bundle dir missing: $BUNDLE_DIR" | |
| fi | |
| - name: Stage macOS release assets | |
| if: startsWith(matrix.platform, 'macos') && (steps.build_macos_primary.outcome == 'success' || steps.build_macos_retry.outcome == 'success') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| BUNDLE_DIR="${{ steps.resolve_macos_bundle_dir.outputs.bundle_dir }}" | |
| STAGING_DIR="release-assets/${{ matrix.target }}" | |
| if [ -z "$BUNDLE_DIR" ] || [ ! -d "$BUNDLE_DIR" ]; then | |
| echo "Expected bundle dir missing: $BUNDLE_DIR" >&2 | |
| exit 1 | |
| fi | |
| assets=() | |
| while IFS= read -r asset; do | |
| [ -n "$asset" ] || continue | |
| assets+=("$asset") | |
| done < <( | |
| find "$BUNDLE_DIR" -type f \ | |
| \( -name "*.dmg" -o -name "*.app.tar.gz" -o -name "*.app.tar.gz.sig" -o -name "latest*.json" \) \ | |
| | sort | |
| ) | |
| if [ "${#assets[@]}" -eq 0 ]; then | |
| echo "No macOS release assets found under $BUNDLE_DIR" >&2 | |
| find "$BUNDLE_DIR" -maxdepth 4 -type f | sort || true | |
| exit 1 | |
| fi | |
| mkdir -p "$STAGING_DIR" | |
| printf 'Staging macOS assets:\n' | |
| printf ' - %s\n' "${assets[@]}" | |
| for asset in "${assets[@]}"; do | |
| cp "$asset" "$STAGING_DIR/$(basename "$asset")" | |
| done | |
| printf 'Staged macOS assets under %s:\n' "$STAGING_DIR" | |
| find "$STAGING_DIR" -maxdepth 1 -type f | sort | |
| - name: Build lime-cli release binary | |
| if: matrix.build_cli && (matrix.platform == 'windows-2022' || matrix.platform == 'ubuntu-22.04' || (startsWith(matrix.platform, 'macos') && (steps.build_macos_primary.outcome == 'success' || steps.build_macos_retry.outcome == 'success'))) | |
| shell: bash | |
| env: | |
| CARGO_PROFILE_RELEASE_LTO: "off" | |
| CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32 | |
| CARGO_INCREMENTAL: 0 | |
| CARGO_TARGET_DIR: src-tauri/target | |
| SCCACHE_GHA_ENABLED: "true" | |
| RUSTC_WRAPPER: sccache | |
| run: | | |
| set -euxo pipefail | |
| cargo build --manifest-path src-tauri/Cargo.toml -p lime-cli --release --target "${{ matrix.target }}" | |
| - name: Package lime-cli release asset | |
| id: package_lime_cli | |
| if: matrix.build_cli && (matrix.platform == 'windows-2022' || matrix.platform == 'ubuntu-22.04' || (startsWith(matrix.platform, 'macos') && (steps.build_macos_primary.outcome == 'success' || steps.build_macos_retry.outcome == 'success'))) | |
| shell: bash | |
| run: | | |
| set -euxo pipefail | |
| VERSION="${{ github.event.inputs.version || github.ref_name }}" | |
| VERSION="${VERSION#v}" | |
| metadata="$( | |
| node packages/lime-cli-npm/scripts/build-release.js \ | |
| --target-triple "${{ matrix.target }}" \ | |
| --version "$VERSION" \ | |
| --out-dir "packages/lime-cli-npm/dist" \ | |
| --json | |
| )" | |
| echo "$metadata" | |
| asset_path="$(node -e 'const data = JSON.parse(process.argv[1]); process.stdout.write(data.archivePath);' "$metadata")" | |
| echo "asset_path=$asset_path" >> "$GITHUB_OUTPUT" | |
| - name: Stage lime-cli release asset | |
| if: matrix.build_cli && (matrix.platform == 'windows-2022' || matrix.platform == 'ubuntu-22.04' || (startsWith(matrix.platform, 'macos') && (steps.build_macos_primary.outcome == 'success' || steps.build_macos_retry.outcome == 'success'))) | |
| shell: bash | |
| run: | | |
| set -euxo pipefail | |
| ASSET_PATH="${{ steps.package_lime_cli.outputs.asset_path }}" | |
| STAGING_DIR="release-assets/${{ matrix.target }}" | |
| if [ -z "$ASSET_PATH" ] || [ ! -f "$ASSET_PATH" ]; then | |
| echo "lime-cli asset missing: $ASSET_PATH" >&2 | |
| exit 1 | |
| fi | |
| node -e " | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const src = process.argv[1]; | |
| const destDir = process.argv[2]; | |
| fs.mkdirSync(destDir, { recursive: true }); | |
| const dest = path.join(destDir, path.basename(src)); | |
| fs.copyFileSync(src, dest); | |
| process.stdout.write(dest); | |
| " "$ASSET_PATH" "$STAGING_DIR" | |
| echo | |
| - name: Upload staged release assets artifact | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-assets-${{ matrix.target }} | |
| path: release-assets | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| # 注意:移除了 Post-build cleanup 步骤 | |
| # 之前的清理会删除 deps/build/incremental 目录,导致缓存无法复用 | |
| # 保留这些文件可以让 rust-cache 更好地工作 | |
| - name: Show sccache stats | |
| run: sccache --show-stats | |
| # 清理 keychain | |
| - name: Cleanup Keychain | |
| if: startsWith(matrix.platform, 'macos') && always() | |
| run: | | |
| security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true | |
| publish_release_assets: | |
| name: Publish staged release assets | |
| needs: build | |
| if: needs.build.result == 'success' | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Download staged release assets | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: release-assets-* | |
| path: release-assets | |
| merge-multiple: true | |
| - name: Inspect staged release assets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ ! -d "release-assets" ]; then | |
| echo "release-assets directory is missing" >&2 | |
| exit 1 | |
| fi | |
| assets=() | |
| while IFS= read -r asset; do | |
| [ -n "$asset" ] || continue | |
| assets+=("$asset") | |
| done < <(find "release-assets" -type f | sort) | |
| if [ "${#assets[@]}" -eq 0 ]; then | |
| echo "No staged release assets found" >&2 | |
| exit 1 | |
| fi | |
| printf 'Downloaded staged release assets:\n' | |
| printf ' - %s\n' "${assets[@]}" | |
| - name: Upload staged release assets to GitHub Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ github.event.inputs.version || github.ref_name }}" | |
| assets=() | |
| while IFS= read -r asset; do | |
| [ -n "$asset" ] || continue | |
| assets+=("$asset") | |
| done < <(find "release-assets" -type f | sort) | |
| if [ "${#assets[@]}" -eq 0 ]; then | |
| echo "No staged release assets found" >&2 | |
| exit 1 | |
| fi | |
| printf 'Uploading staged release assets:\n' | |
| printf ' - %s\n' "${assets[@]}" | |
| gh release upload "$TAG" "${assets[@]}" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --clobber |