mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
938 lines
36 KiB
YAML
938 lines
36 KiB
YAML
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: 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: Build macOS app (fallback without notarization, verbose)
|
|
id: build_macos_fallback
|
|
if: startsWith(matrix.platform, 'macos') && steps.build_macos_primary.outcome == 'failure' && steps.build_macos_retry.outcome == 'failure'
|
|
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 }}
|
|
run: |
|
|
set -euxo pipefail
|
|
echo "Fallback macOS target without notarization: ${{ matrix.target }}"
|
|
pnpm tauri build --target "${{ matrix.target }}"
|
|
|
|
- name: Warn on macOS notarization fallback
|
|
if: startsWith(matrix.platform, 'macos') && steps.build_macos_primary.outcome == 'failure' && steps.build_macos_retry.outcome == 'failure'
|
|
run: |
|
|
echo "::warning::macOS notarization failed twice; fallback build skipped notarization and produced a signed-only artifact."
|
|
|
|
- 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' || steps.build_macos_fallback.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: 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' || steps.build_macos_fallback.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' || steps.build_macos_fallback.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' || steps.build_macos_fallback.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' || steps.build_macos_fallback.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
|