name: Build and Release Desktop Apps
on:
push:
tags:
- 'v*'
jobs:
# ============================================
# 第1阶段:并行构建所有平台
# ============================================
build-macos:
name: Build macOS
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Configure git for HTTPS
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git config --global url."https://".insteadOf "ssh://"
- name: Install dependencies
run: pnpm install
- name: Build all packages
run: pnpm build
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }}
p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }}
- name: Build macOS app
working-directory: apps/desktop
run: |
echo "Building macOS versions..."
pnpm build
npx electron-builder --mac --x64 --arm64 --publish never
env:
CSC_LINK: ${{ secrets.MACOS_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
CSC_IDENTITY_AUTO_DISCOVERY: true
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload macOS artifacts for signing
uses: actions/upload-artifact@v4
with:
name: macos-unsigned
path: |
apps/desktop/release/*.dmg
apps/desktop/release/*.zip
apps/desktop/release/latest-mac.yml
if-no-files-found: error
retention-days: 1
build-windows:
name: Build Windows
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Configure git for HTTPS
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git config --global url."https://".insteadOf "ssh://"
- name: Install dependencies
run: pnpm install
- name: Build all packages
run: pnpm build
- name: Build Windows app
working-directory: apps/desktop
run: |
pnpm build
npx electron-builder --win --publish never
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Windows artifacts for signing
uses: actions/upload-artifact@v4
with:
name: windows-unsigned
path: |
apps/desktop/release/*setup*.exe
apps/desktop/release/latest.yml
if-no-files-found: error
retention-days: 1
build-linux:
name: Build Linux
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Configure git for HTTPS
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git config --global url."https://".insteadOf "ssh://"
- name: Install dependencies
run: pnpm install
- name: Build all packages
run: pnpm build
- name: Build Linux app
working-directory: apps/desktop
run: |
pnpm build
npx electron-builder --linux --publish never
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Linux artifacts (no signing needed)
uses: actions/upload-artifact@v4
with:
name: linux-final
path: |
apps/desktop/release/*.deb
apps/desktop/release/*.AppImage
apps/desktop/release/*.rpm
apps/desktop/release/latest-linux.yml
if-no-files-found: error
retention-days: 1
# ============================================
# 第2阶段:签名和公证
# ============================================
sign-macos:
name: Notarize macOS
needs: build-macos
runs-on: macos-latest
if: success()
steps:
- name: Check Apple credentials
id: check_credentials
run: |
if [[ -z "${{ secrets.APPLE_ID }}" ]] || \
[[ -z "${{ secrets.APPLE_ID_PASSWORD }}" ]] || \
[[ -z "${{ secrets.APPLE_TEAM_ID }}" ]]; then
echo "Missing Apple credentials, skipping notarization"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Apple credentials found"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Download macOS artifacts
if: steps.check_credentials.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
name: macos-unsigned
path: macos-files
- name: Submit for notarization
if: steps.check_credentials.outputs.skip != 'true'
id: submit
run: |
cd macos-files
submission_info=""
for dmg in *.dmg; do
if [ -f "$dmg" ]; then
echo "=========================================="
echo "📦 Processing: $dmg"
echo "📊 File size: $(ls -lh "$dmg" | awk '{print $5}')"
echo "=========================================="
echo "🚀 Submitting $dmg for notarization..."
echo " Apple ID: ${{ secrets.APPLE_ID }}"
echo " Team ID: ${{ secrets.APPLE_TEAM_ID }}"
# 提交公证 - 添加详细输出
set +e # 不立即退出
output=$(xcrun notarytool submit "$dmg" \
--apple-id "${{ secrets.APPLE_ID }}" \
--password "${{ secrets.APPLE_ID_PASSWORD }}" \
--team-id "${{ secrets.APPLE_TEAM_ID }}" \
--wait 2>&1)
exit_code=$?
set -e
echo "📋 Notarization output:"
echo "$output"
echo ""
echo "Exit code: $exit_code"
# 检查公证状态
if [ $exit_code -eq 0 ] && echo "$output" | grep -q "Accepted"; then
echo "✅ Notarization accepted for $dmg"
# Staple the ticket
echo "🔖 Stapling ticket to $dmg..."
xcrun stapler staple "$dmg"
echo "✅ Successfully stapled $dmg"
else
echo "❌ Notarization failed for $dmg"
echo "Full output above ⬆️"
# Extract submission ID if available
submission_id=$(echo "$output" | grep -o 'id: [a-f0-9-]*' | head -1 | cut -d' ' -f2)
if [ -n "$submission_id" ]; then
echo "📝 Submission ID: $submission_id"
echo "Fetching detailed log..."
xcrun notarytool log "$submission_id" \
--apple-id "${{ secrets.APPLE_ID }}" \
--password "${{ secrets.APPLE_ID_PASSWORD }}" \
--team-id "${{ secrets.APPLE_TEAM_ID }}" || true
fi
exit 1
fi
fi
done
- name: Upload notarized macOS artifacts
uses: actions/upload-artifact@v4
with:
name: macos-final
path: |
macos-files/*.dmg
macos-files/*.zip
macos-files/latest-mac.yml
if-no-files-found: error
retention-days: 1
sign-windows:
name: Sign Windows with SignPath
needs: build-windows
runs-on: ubuntu-latest
if: success()
permissions:
id-token: write
steps:
- name: Check SignPath credentials
id: check_credentials
run: |
if [[ -z "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" ]] || \
[[ -z "${{ secrets.SIGNPATH_PROJECT_SLUG }}" ]] || \
[[ -z "${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}" ]]; then
echo "Missing SignPath credentials, skipping signing"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "SignPath credentials found"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Download Windows artifacts
uses: actions/download-artifact@v4
with:
name: windows-unsigned
path: windows-files
- name: Upload installer for SignPath
if: steps.check_credentials.outputs.skip != 'true'
uses: actions/upload-artifact@v4
id: upload-for-signing
with:
name: windows-installer-to-sign
path: windows-files/*setup*.exe
if-no-files-found: error
- name: Sign with SignPath
if: steps.check_credentials.outputs.skip != 'true'
uses: SignPath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIG_SLUG }}
github-artifact-id: ${{ steps.upload-for-signing.outputs.artifact-id }}
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 86400 # 24 hours
output-artifact-directory: signed
- name: Generate latest.yml for signed installer
if: steps.check_credentials.outputs.skip != 'true'
run: |
cd signed
# 获取签名后的文件
signed_exe=$(ls *.exe | head -1)
if [ -z "$signed_exe" ]; then
echo "No signed exe found, using unsigned"
cd ../windows-files
signed_exe=$(ls *setup*.exe | head -1)
fi
echo "Generating latest.yml for $signed_exe..."
# 计算 SHA512 - 确保单行输出,移除所有换行符
sha512_hash=$(sha512sum "$signed_exe" | awk '{print $1}' | xxd -r -p | base64 | tr -d '\n')
# 获取文件大小
file_size=$(stat -c%s "$signed_exe" 2>/dev/null || stat -f%z "$signed_exe" 2>/dev/null)
# 从文件名提取版本号
version=$(echo "$signed_exe" | sed -E 's/promptx-desktop-([0-9.]+)-.*/\1/')
# 获取发布日期
release_date=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
# 创建 latest.yml - 使用双引号包裹 sha512 确保单行
printf "version: %s\nfiles:\n - url: %s\n sha512: \"%s\"\n size: %s\npath: %s\nsha512: \"%s\"\nreleaseDate: '%s'\n" \
"${version}" "${signed_exe}" "${sha512_hash}" "${file_size}" \
"${signed_exe}" "${sha512_hash}" "${release_date}" > latest.yml
# 验证生成的 YAML 格式
echo "Generated latest.yml:"
cat latest.yml
# 验证 YAML 语法
if command -v python3 &> /dev/null; then
python3 -c "import yaml; yaml.safe_load(open('latest.yml'))" && echo "✅ YAML syntax valid" || echo "❌ YAML syntax invalid"
fi
# 将签名的文件移回主目录
if [ -f "$signed_exe" ] && [ "$(pwd)" = "*/signed" ]; then
mv "$signed_exe" ../windows-files/
mv latest.yml ../windows-files/
cd ../windows-files
fi
- name: Prepare final Windows artifacts
if: steps.check_credentials.outputs.skip != 'true'
run: |
mkdir -p windows-final-files
# 复制签名后的文件
if [ -d "signed" ] && [ -n "$(ls -A signed/*.exe 2>/dev/null)" ]; then
echo "Using signed files"
cp signed/*.exe windows-final-files/
cp signed/latest.yml windows-final-files/ || true
else
echo "Using original files (signing might have been skipped)"
cp windows-files/*setup*.exe windows-final-files/
cp windows-files/latest.yml windows-final-files/ || true
fi
echo "Final Windows files:"
ls -la windows-final-files/
- name: Upload signed Windows artifacts
uses: actions/upload-artifact@v4
with:
name: windows-final
path: windows-final-files/*
if-no-files-found: error
retention-days: 1
# ============================================
# 第3阶段:统一上传到 GitHub Release
# ============================================
upload-release:
name: Upload to GitHub Release
needs: [sign-macos, sign-windows, build-linux]
runs-on: ubuntu-latest
if: success()
permissions:
contents: write
steps:
- name: Download all final artifacts
uses: actions/download-artifact@v4
- name: Prepare release files
run: |
# 创建发布目录
mkdir -p release-files
# 移动所有文件到发布目录
if [ -d "macos-final" ]; then
mv macos-final/* release-files/ || true
fi
if [ -d "windows-final" ]; then
mv windows-final/* release-files/ || true
fi
if [ -d "linux-final" ]; then
mv linux-final/* release-files/ || true
fi
# 列出所有要发布的文件
echo "Files to release:"
ls -la release-files/
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
name: Release ${{ github.ref_name }}
generate_release_notes: true
draft: false
prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'rc') }}
files: release-files/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release files as artifact
uses: actions/upload-artifact@v4
with:
name: release-files
path: release-files/*
retention-days: 1
- name: Summary
run: |
echo "## 🎉 Desktop Apps Released Successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Release: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### ✅ Completed Steps:" >> $GITHUB_STEP_SUMMARY
echo "- macOS: Built and Notarized" >> $GITHUB_STEP_SUMMARY
echo "- Windows: Built and Signed" >> $GITHUB_STEP_SUMMARY
echo "- Linux: Built (deb, AppImage, rpm)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📦 Release Contents:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
ls -lh release-files/ >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
# ============================================
# 第4阶段:同步到 Cloudflare R2 CDN
# ============================================
sync-to-r2:
name: Sync to R2 CDN
needs: upload-release
runs-on: ubuntu-latest
if: success()
steps:
- name: Download release files
uses: actions/download-artifact@v4
with:
name: release-files
path: release-files
- name: Upload to Cloudflare R2
run: |
# 安装 AWS CLI(用于 S3 兼容 API)
pip install awscli
# 提取版本号(去掉 v 前缀)
VERSION=${GITHUB_REF_NAME#v}
# 配置 R2 endpoint
R2_ENDPOINT="https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com"
# 上传所有发布文件到 R2(仅版本目录)
for file in release-files/*; do
if [ -f "$file" ]; then
filename=$(basename "$file")
echo "Uploading $filename to R2..."
# 上传到版本目录
aws s3 cp "$file" "s3://promptx-releases/$VERSION/$filename" \
--endpoint-url "$R2_ENDPOINT"
fi
done
echo "✅ All files uploaded to R2: promptx-releases/$VERSION/"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
- name: Summary
run: |
VERSION=${GITHUB_REF_NAME#v}
echo "## 🌐 R2 CDN Sync Complete!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📦 Version: $VERSION" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔗 CDN URLs:" >> $GITHUB_STEP_SUMMARY
echo "- Version: https://r2.deepractice.ai/promptx-releases/$VERSION/" >> $GITHUB_STEP_SUMMARY
echo "- Latest: https://r2.deepractice.ai/promptx-releases/latest/ (auto-redirects)" >> $GITHUB_STEP_SUMMARY