mirror of
https://github.com/Hommy-master/capcut-mate.git
synced 2026-10-30 18:44:19 +08:00
优化打包公证流程。
This commit is contained in:
@@ -38,29 +38,10 @@ jobs:
|
||||
echo "desktop=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
build-windows:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.desktop == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# 固定 runner 大版本,避免 -latest 自动迁移导致构建环境突变
|
||||
- os: windows-2022
|
||||
platform: win
|
||||
artifact_name: windows-build
|
||||
installer_command: build-win-installer
|
||||
- os: macos-14
|
||||
platform: mac
|
||||
artifact_name: macos-build
|
||||
installer_command: build-mac-installer
|
||||
- os: macos-15-intel
|
||||
platform: mac-x64
|
||||
artifact_name: macos-build-x64
|
||||
installer_command: build-mac-x64-installer
|
||||
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- name: 准备代码
|
||||
uses: actions/checkout@v4
|
||||
@@ -72,35 +53,56 @@ jobs:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: desktop-client/package-lock.json
|
||||
|
||||
- name: 打印构建环境信息
|
||||
run: |
|
||||
echo "Runner OS: ${{ runner.os }}"
|
||||
echo "Runner image: ${{ matrix.os }}"
|
||||
node -v
|
||||
npm -v
|
||||
- name: 安装依赖并构建
|
||||
working-directory: ./desktop-client
|
||||
shell: bash
|
||||
run: |
|
||||
npm ci
|
||||
npm --prefix web ci
|
||||
npm --prefix web run build
|
||||
node scripts/update-version.js
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
|
||||
- name: 安装Electron依赖
|
||||
run: npm ci
|
||||
- name: 创建 Windows 安装包
|
||||
working-directory: ./desktop-client
|
||||
shell: bash
|
||||
run: npm run build-win-installer
|
||||
timeout-minutes: 45
|
||||
|
||||
- name: 安装前端依赖
|
||||
run: npm ci
|
||||
working-directory: ./desktop-client/web
|
||||
- name: 上传 Windows 安装包
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-installer
|
||||
path: desktop-client/dist/capcut-mate-windows-x64-installer.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: 构建前端
|
||||
run: npm run build
|
||||
working-directory: ./desktop-client/web
|
||||
build-mac-arm64:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.desktop == 'true'
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: 准备代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 更新版本号
|
||||
run: node scripts/update-version.js
|
||||
- name: 设置Node.js环境
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.14.0'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: desktop-client/package-lock.json
|
||||
|
||||
- name: 安装依赖并构建
|
||||
working-directory: ./desktop-client
|
||||
run: |
|
||||
npm ci
|
||||
npm --prefix web ci
|
||||
npm --prefix web run build
|
||||
node scripts/update-version.js
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
|
||||
# 预导入证书并授予 codesign 无 UI 权限,避免 electron-builder 临时钥匙串缺 codesign: 导致挂起
|
||||
- name: 导入 macOS 签名证书
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
@@ -124,63 +126,207 @@ jobs:
|
||||
-S apple-tool:,apple:,codesign: \
|
||||
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
# 把签名钥匙串放到搜索列表最前
|
||||
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g')
|
||||
|
||||
echo "Imported signing identities:"
|
||||
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||
|
||||
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
||||
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"
|
||||
rm -f "$CERTIFICATE_PATH"
|
||||
|
||||
- name: 根据平台创建安装包
|
||||
timeout-minutes: 75
|
||||
- name: 准备 App Store Connect API Key
|
||||
env:
|
||||
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${APPLE_API_KEY_BASE64:-}" ] || [ -z "${APPLE_API_KEY_ID:-}" ] || [ -z "${APPLE_API_ISSUER:-}" ]; then
|
||||
echo "Missing required secrets: APPLE_API_KEY_BASE64 / APPLE_API_KEY_ID / APPLE_API_ISSUER"
|
||||
exit 1
|
||||
fi
|
||||
API_KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
|
||||
echo "$APPLE_API_KEY_BASE64" | openssl base64 -d -A -out "$API_KEY_PATH"
|
||||
chmod 600 "$API_KEY_PATH"
|
||||
if ! grep -q "BEGIN PRIVATE KEY" "$API_KEY_PATH"; then
|
||||
echo "Decoded APPLE_API_KEY_BASE64 is not a valid .p8 private key"
|
||||
exit 1
|
||||
fi
|
||||
echo "APPLE_API_KEY=$API_KEY_PATH" >> "$GITHUB_ENV"
|
||||
echo "API key ready: $API_KEY_PATH"
|
||||
|
||||
- name: 创建 macOS arm64 安装包
|
||||
timeout-minutes: 120
|
||||
working-directory: ./desktop-client
|
||||
env:
|
||||
# 不传 CSC_LINK:使用上一步导入的钥匙串,避免再建缺 codesign: 的临时钥匙串
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: ${{ startsWith(matrix.os, 'macos') && 'true' || 'false' }}
|
||||
APPLE_ID: ${{ startsWith(matrix.os, 'macos') && secrets.APPLE_ID || '' }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ startsWith(matrix.os, 'macos') && secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }}
|
||||
APPLE_TEAM_ID: ${{ startsWith(matrix.os, 'macos') && secrets.APPLE_TEAM_ID || '' }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: 'true'
|
||||
NOTARIZE_REQUIRE_API_KEY: '1'
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
MAC_BUILD_MAX_ATTEMPTS: '3'
|
||||
run: |
|
||||
if [ -n "${KEYCHAIN_PATH:-}" ]; then
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
fi
|
||||
npm run ${{ matrix.installer_command }}
|
||||
shell: bash
|
||||
chmod +x scripts/ci-build-mac.sh
|
||||
scripts/ci-build-mac.sh build-mac-installer
|
||||
|
||||
- name: 清理 macOS 签名钥匙串
|
||||
if: always() && startsWith(matrix.os, 'macos')
|
||||
- name: 清理 macOS 凭据
|
||||
if: always()
|
||||
run: |
|
||||
if [ -n "${KEYCHAIN_PATH:-}" ]; then
|
||||
security delete-keychain "$KEYCHAIN_PATH" || true
|
||||
fi
|
||||
if [ -n "${APPLE_API_KEY:-}" ]; then
|
||||
rm -f "$APPLE_API_KEY" || true
|
||||
fi
|
||||
|
||||
- name: 列出dist目录内容
|
||||
run: |
|
||||
echo "当前操作系统: ${{ runner.os }}"
|
||||
ls desktop-client/dist/
|
||||
shell: bash
|
||||
|
||||
- name: 上传安装包构件
|
||||
- name: 上传 macOS arm64 安装包
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}-installer
|
||||
path: desktop-client/dist/
|
||||
if-no-files-found: ignore
|
||||
name: macos-arm64-installer
|
||||
path: desktop-client/dist/capcut-mate-macos-arm64-installer.dmg
|
||||
if-no-files-found: error
|
||||
|
||||
- name: 发布版本
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
# 等 arm64 公证结束后再跑 x64,降低同时打 Apple 公证接口导致超时的概率
|
||||
build-mac-x64:
|
||||
needs: [changes, build-mac-arm64]
|
||||
if: |
|
||||
always() &&
|
||||
needs.changes.outputs.desktop == 'true' &&
|
||||
needs.build-mac-arm64.result != 'cancelled'
|
||||
runs-on: macos-15-intel
|
||||
steps:
|
||||
- name: 准备代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置Node.js环境
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.14.0'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: desktop-client/package-lock.json
|
||||
|
||||
- name: 安装依赖并构建
|
||||
working-directory: ./desktop-client
|
||||
run: |
|
||||
npm ci
|
||||
npm --prefix web ci
|
||||
npm --prefix web run build
|
||||
node scripts/update-version.js
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
|
||||
- name: 导入 macOS 签名证书
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CERTIFICATE_PATH="$RUNNER_TEMP/signing-cert.p12"
|
||||
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
|
||||
KEYCHAIN_PASSWORD="$(openssl rand -base64 32)"
|
||||
|
||||
echo "$CSC_LINK" | openssl base64 -d -A -out "$CERTIFICATE_PATH"
|
||||
|
||||
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 "$CERTIFICATE_PATH" -P "$CSC_KEY_PASSWORD" \
|
||||
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" \
|
||||
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
||||
|
||||
security set-key-partition-list \
|
||||
-S apple-tool:,apple:,codesign: \
|
||||
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g')
|
||||
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||
|
||||
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
||||
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"
|
||||
rm -f "$CERTIFICATE_PATH"
|
||||
|
||||
- name: 准备 App Store Connect API Key
|
||||
env:
|
||||
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${APPLE_API_KEY_BASE64:-}" ] || [ -z "${APPLE_API_KEY_ID:-}" ] || [ -z "${APPLE_API_ISSUER:-}" ]; then
|
||||
echo "Missing required secrets: APPLE_API_KEY_BASE64 / APPLE_API_KEY_ID / APPLE_API_ISSUER"
|
||||
exit 1
|
||||
fi
|
||||
API_KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
|
||||
echo "$APPLE_API_KEY_BASE64" | openssl base64 -d -A -out "$API_KEY_PATH"
|
||||
chmod 600 "$API_KEY_PATH"
|
||||
if ! grep -q "BEGIN PRIVATE KEY" "$API_KEY_PATH"; then
|
||||
echo "Decoded APPLE_API_KEY_BASE64 is not a valid .p8 private key"
|
||||
exit 1
|
||||
fi
|
||||
echo "APPLE_API_KEY=$API_KEY_PATH" >> "$GITHUB_ENV"
|
||||
echo "API key ready: $API_KEY_PATH"
|
||||
|
||||
- name: 创建 macOS x64 安装包
|
||||
timeout-minutes: 120
|
||||
working-directory: ./desktop-client
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: 'true'
|
||||
NOTARIZE_REQUIRE_API_KEY: '1'
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
MAC_BUILD_MAX_ATTEMPTS: '3'
|
||||
run: |
|
||||
chmod +x scripts/ci-build-mac.sh
|
||||
scripts/ci-build-mac.sh build-mac-x64-installer
|
||||
|
||||
- name: 清理 macOS 凭据
|
||||
if: always()
|
||||
run: |
|
||||
if [ -n "${KEYCHAIN_PATH:-}" ]; then
|
||||
security delete-keychain "$KEYCHAIN_PATH" || true
|
||||
fi
|
||||
if [ -n "${APPLE_API_KEY:-}" ]; then
|
||||
rm -f "$APPLE_API_KEY" || true
|
||||
fi
|
||||
|
||||
- name: 上传 macOS x64 安装包
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-x64-installer
|
||||
path: desktop-client/dist/capcut-mate-macos-x64-installer.dmg
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
needs: [changes, build-windows, build-mac-arm64, build-mac-x64]
|
||||
if: |
|
||||
always() &&
|
||||
startsWith(github.ref, 'refs/tags/v') &&
|
||||
needs.changes.outputs.desktop == 'true' &&
|
||||
needs.build-windows.result == 'success' &&
|
||||
needs.build-mac-arm64.result == 'success' &&
|
||||
needs.build-mac-x64.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 下载全部安装包
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: release-assets
|
||||
|
||||
- name: 整理发布文件
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist
|
||||
find release-assets -type f \( -name '*.exe' -o -name '*.dmg' \) -print -exec cp {} dist/ \;
|
||||
ls -lh dist/
|
||||
|
||||
- name: 发布到 GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: Release ${{ github.ref_name }}
|
||||
files: |
|
||||
desktop-client/dist/capcut-mate-windows-x64-installer.exe
|
||||
desktop-client/dist/capcut-mate-macos-arm64-installer.dmg
|
||||
desktop-client/dist/capcut-mate-macos-x64-installer.dmg
|
||||
dist/capcut-mate-windows-x64-installer.exe
|
||||
dist/capcut-mate-macos-arm64-installer.dmg
|
||||
dist/capcut-mate-macos-x64-installer.dmg
|
||||
overwrite: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# macOS CI:安装包构建(含签名/公证/DMG),对 DMG Resource busy 等偶发错误自动重试
|
||||
set -euo pipefail
|
||||
|
||||
INSTALLER_COMMAND="${1:?installer npm script required}"
|
||||
MAX_ATTEMPTS="${MAC_BUILD_MAX_ATTEMPTS:-3}"
|
||||
|
||||
cleanup_dmg_devices() {
|
||||
echo "Cleaning up attached DMG devices (best effort)..."
|
||||
# 卸下可能残留的临时磁盘,减轻 hdiutil Resource busy
|
||||
hdiutil info 2>/dev/null | awk '/\/dev\/disk[0-9]+/ {print $1}' | sort -u | while read -r disk; do
|
||||
hdiutil detach -force "$disk" >/dev/null 2>&1 || true
|
||||
done || true
|
||||
sleep 2
|
||||
}
|
||||
|
||||
unlock_signing_keychain() {
|
||||
if [ -n "${KEYCHAIN_PATH:-}" ] && [ -n "${KEYCHAIN_PASSWORD:-}" ]; then
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" || true
|
||||
security set-key-partition-list \
|
||||
-S apple-tool:,apple:,codesign: \
|
||||
-s -k "$KEYCHAIN_PASSWORD" \
|
||||
"$KEYCHAIN_PATH" || true
|
||||
fi
|
||||
}
|
||||
|
||||
is_retryable_build_error() {
|
||||
local log_file="$1"
|
||||
grep -Eiq \
|
||||
'Resource busy|Unable to detach device|couldn.?t eject|hdiutil|DMGError|ECONNRESET|ETIMEDOUT|timed out|offline|No network route' \
|
||||
"$log_file"
|
||||
}
|
||||
|
||||
LOG_FILE="$(mktemp)"
|
||||
trap 'rm -f "$LOG_FILE"' EXIT
|
||||
|
||||
for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
|
||||
echo "==> mac installer attempt ${attempt}/${MAX_ATTEMPTS}: npm run ${INSTALLER_COMMAND}"
|
||||
unlock_signing_keychain
|
||||
cleanup_dmg_devices
|
||||
|
||||
set +e
|
||||
npm run "$INSTALLER_COMMAND" 2>&1 | tee "$LOG_FILE"
|
||||
status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "==> mac installer succeeded on attempt ${attempt}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> mac installer failed on attempt ${attempt} (exit ${status})"
|
||||
if [ "$attempt" -ge "$MAX_ATTEMPTS" ]; then
|
||||
exit "$status"
|
||||
fi
|
||||
if ! is_retryable_build_error "$LOG_FILE"; then
|
||||
echo "==> error does not look retryable; stopping"
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
|
||||
exit 1
|
||||
@@ -1,64 +1,146 @@
|
||||
/**
|
||||
* macOS 公证(带重试)
|
||||
* 解决 Apple notarytool 偶发返回非 JSON 的 HTTP 错误导致 CI 失败。
|
||||
* 优先使用 App Store Connect API Key;未配置时回退 Apple ID + App 专用密码。
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execFileSync } = require("child_process");
|
||||
|
||||
function buildNotarizeAuth() {
|
||||
const appleApiKey = process.env.APPLE_API_KEY;
|
||||
const appleApiKeyId = process.env.APPLE_API_KEY_ID;
|
||||
const appleApiIssuer = process.env.APPLE_API_ISSUER;
|
||||
|
||||
if (appleApiKey && appleApiKeyId) {
|
||||
if (!fs.existsSync(appleApiKey)) {
|
||||
throw new Error(`APPLE_API_KEY file not found: ${appleApiKey}`);
|
||||
}
|
||||
const pem = fs.readFileSync(appleApiKey, "utf8");
|
||||
if (!pem.includes("BEGIN PRIVATE KEY")) {
|
||||
throw new Error(
|
||||
"APPLE_API_KEY does not look like a valid .p8 private key (missing BEGIN PRIVATE KEY)"
|
||||
);
|
||||
}
|
||||
const auth = {
|
||||
appleApiKey,
|
||||
appleApiKeyId
|
||||
};
|
||||
// Team Key 需要 Issuer ID;个人账号开通 API 后一般也是 Team Key
|
||||
if (appleApiIssuer) {
|
||||
auth.appleApiIssuer = appleApiIssuer;
|
||||
} else {
|
||||
console.warn(
|
||||
"APPLE_API_ISSUER is empty; Team API Key normally requires Issuer ID"
|
||||
);
|
||||
}
|
||||
return { mode: "apiKey", auth };
|
||||
}
|
||||
|
||||
const appleId = process.env.APPLE_ID;
|
||||
const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD;
|
||||
const teamId = process.env.APPLE_TEAM_ID;
|
||||
if (appleId && appleIdPassword && teamId) {
|
||||
return {
|
||||
mode: "appleId",
|
||||
auth: { appleId, appleIdPassword, teamId }
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function unlockKeychainIfNeeded() {
|
||||
const keychainPath = process.env.KEYCHAIN_PATH;
|
||||
const keychainPassword = process.env.KEYCHAIN_PASSWORD;
|
||||
if (!keychainPath || !keychainPassword) return;
|
||||
try {
|
||||
execFileSync("security", ["unlock-keychain", "-p", keychainPassword, keychainPath], {
|
||||
stdio: "inherit"
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("unlock-keychain failed:", err && err.message ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
function isTransientNotarizeError(message) {
|
||||
const text = String(message || "").toLowerCase();
|
||||
return (
|
||||
text.includes("timed out") ||
|
||||
text.includes("timeout") ||
|
||||
text.includes("offline") ||
|
||||
text.includes("no network route") ||
|
||||
text.includes("connection") ||
|
||||
text.includes("httperror") ||
|
||||
text.includes("socket") ||
|
||||
text.includes("econnreset") ||
|
||||
text.includes("enotfound") ||
|
||||
text.includes("unexpected result")
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = async function notarizeAfterSign(context) {
|
||||
const { electronPlatformName, appOutDir } = context;
|
||||
if (electronPlatformName !== "darwin") return;
|
||||
|
||||
const appleId = process.env.APPLE_ID;
|
||||
const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD;
|
||||
const teamId = process.env.APPLE_TEAM_ID;
|
||||
if (!appleId || !appleIdPassword || !teamId) {
|
||||
console.log("skip notarize: APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID not set");
|
||||
const requireApiKey = process.env.NOTARIZE_REQUIRE_API_KEY === "1";
|
||||
const creds = buildNotarizeAuth();
|
||||
if (!creds) {
|
||||
const msg =
|
||||
"notarize credentials missing: set App Store Connect API Key " +
|
||||
"(APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER) " +
|
||||
"or Apple ID (APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID)";
|
||||
if (requireApiKey) {
|
||||
throw new Error(msg);
|
||||
}
|
||||
console.log(`skip notarize: ${msg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 长时间打包后钥匙串可能再次上锁,公证前先解锁
|
||||
const keychainPath = process.env.KEYCHAIN_PATH;
|
||||
const keychainPassword = process.env.KEYCHAIN_PASSWORD;
|
||||
if (keychainPath && keychainPassword) {
|
||||
try {
|
||||
execFileSync("security", ["unlock-keychain", "-p", keychainPassword, keychainPath], {
|
||||
stdio: "inherit"
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("unlock-keychain failed:", err && err.message ? err.message : err);
|
||||
}
|
||||
if (requireApiKey && creds.mode !== "apiKey") {
|
||||
throw new Error(
|
||||
"NOTARIZE_REQUIRE_API_KEY=1 but API Key is not configured; refusing Apple ID fallback"
|
||||
);
|
||||
}
|
||||
|
||||
unlockKeychainIfNeeded();
|
||||
|
||||
const { notarize } = require("@electron/notarize");
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
const appPath = path.join(appOutDir, `${appName}.app`);
|
||||
|
||||
const maxAttempts = 5;
|
||||
const maxAttempts = 6;
|
||||
let lastError;
|
||||
|
||||
console.log(`notarize auth mode: ${creds.mode}`);
|
||||
if (creds.mode === "apiKey") {
|
||||
console.log(`notarize api key id: ${creds.auth.appleApiKeyId}`);
|
||||
console.log(`notarize api issuer set: ${Boolean(creds.auth.appleApiIssuer)}`);
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
const started = Date.now();
|
||||
try {
|
||||
unlockKeychainIfNeeded();
|
||||
console.log(`notarizing ${appPath} (attempt ${attempt}/${maxAttempts})`);
|
||||
// @electron/notarize v3:提交公证并自动 staple
|
||||
await notarize({
|
||||
appPath,
|
||||
appleId,
|
||||
appleIdPassword,
|
||||
teamId
|
||||
...creds.auth
|
||||
});
|
||||
console.log(`notarization successful in ${Math.round((Date.now() - started) / 1000)}s`);
|
||||
return;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
const message = err && err.message ? err.message : String(err);
|
||||
console.warn(`notarize attempt ${attempt} failed after ${Math.round((Date.now() - started) / 1000)}s: ${message}`);
|
||||
if (attempt < maxAttempts) {
|
||||
const waitSec = attempt * 60;
|
||||
console.log(`waiting ${waitSec}s before retry...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, waitSec * 1000));
|
||||
console.warn(
|
||||
`notarize attempt ${attempt} failed after ${Math.round((Date.now() - started) / 1000)}s: ${message}`
|
||||
);
|
||||
if (attempt >= maxAttempts) break;
|
||||
if (!isTransientNotarizeError(message)) {
|
||||
console.warn("non-transient notarize error, stop retrying");
|
||||
break;
|
||||
}
|
||||
const waitSec = Math.min(180, attempt * 45);
|
||||
console.log(`waiting ${waitSec}s before retry...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, waitSec * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user