chore: initialize qiming workspace repository
This commit is contained in:
27
qimingclaw/.github/workflows/pr.yml
vendored
Normal file
27
qimingclaw/.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Lint PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Validate PR title
|
||||
run: |
|
||||
python .github/scripts/commits.py "${{ github.event.pull_request.title }}"
|
||||
536
qimingclaw/.github/workflows/release-electron-dev.yml
vendored
Normal file
536
qimingclaw/.github/workflows/release-electron-dev.yml
vendored
Normal file
@@ -0,0 +1,536 @@
|
||||
# Electron 桌面客户端 — 预发布版本构建
|
||||
# 通过 prerelease-v* tag 触发,构建全平台安装包并创建 GitHub Draft Release,
|
||||
# 自动同步到阿里云 OSS 并更新 beta/latest.json,供开发测试使用。
|
||||
#
|
||||
# 触发方式:
|
||||
# - 推送 tag 匹配 prerelease-v*(例如 prerelease-v0.8.0)→ 构建 → 自动上传 OSS → 更新 beta/latest.json
|
||||
#
|
||||
# 支持平台:
|
||||
# - macOS arm64(macos-latest)
|
||||
# - macOS x64(macos-latest, cross-compile)
|
||||
# - Windows x64(msi)
|
||||
# - Linux x64(AppImage + deb)
|
||||
# - Linux arm64(AppImage + deb)
|
||||
#
|
||||
# 使用的 Secrets(可与 Tauri 共用 Apple 相关,也可单独配置):
|
||||
# - GH_PAT(可选):创建 Release、上传资产(未配置时自动回退到 github.token,需 Contents: Write)
|
||||
# - macOS 签名/公证(未配置时 Mac 构建为无签名包,用户打开会提示「已损坏」):
|
||||
# - APPLE_TEAM_ID / APPLE_SIGNING_IDENTITY / APPLE_CERTIFICATE(.p12 Base64)/ APPLE_CERTIFICATE_PASSWORD
|
||||
# - 公证:APPLE_API_KEY(.p8 Base64)/ APPLE_API_KEY_ID / APPLE_ISSUER_ID
|
||||
# - Windows:本 workflow 仅构建产物,不在 CI 内签名;签名在本地 Windows(SimplySign)手动完成
|
||||
# - OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET:上传到阿里云 OSS
|
||||
#
|
||||
# 产物目录:crates/agent-electron-client/release/${version}/(由 package.json directories.output 决定)
|
||||
|
||||
name: Release Electron App (Prerelease)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "prerelease-v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#prerelease-v}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Electron prerelease version: $VERSION"
|
||||
|
||||
- name: Read release notes
|
||||
id: notes
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TAG="${{ github.ref_name }}"
|
||||
NOTES_FILE="release-notes/${TAG}.md"
|
||||
if [ -f "$NOTES_FILE" ]; then
|
||||
echo "Using release notes from $NOTES_FILE"
|
||||
cp "$NOTES_FILE" release_notes.txt
|
||||
else
|
||||
echo "QimingClaw ${VERSION} (Prerelease)。请在 Assets 中下载对应平台安装包。" > release_notes.txt
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" 2>/dev/null; then
|
||||
echo "Release $TAG already exists"
|
||||
else
|
||||
gh release create "$TAG" \
|
||||
--title "QimingClaw (Electron) ${{ steps.version.outputs.version }} - Prerelease" \
|
||||
--notes-file release_notes.txt \
|
||||
--draft \
|
||||
--prerelease \
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
echo "Created release $TAG"
|
||||
fi
|
||||
|
||||
build-electron:
|
||||
name: Build Electron (${{ matrix.platform }} ${{ matrix.arch }})
|
||||
needs: prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: macos-latest
|
||||
arch: arm64
|
||||
dist_cmd: dist:mac:arm64
|
||||
- platform: macos-latest
|
||||
arch: x64
|
||||
dist_cmd: dist:mac:x64
|
||||
- platform: windows-latest
|
||||
arch: x64
|
||||
dist_cmd: dist:win
|
||||
- platform: ubuntu-24.04
|
||||
arch: x64
|
||||
dist_cmd: dist:linux:x64
|
||||
|
||||
- platform: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
dist_cmd: dist:linux:arm64
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set version in package.json
|
||||
shell: bash
|
||||
working-directory: crates/agent-electron-client
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
npm pkg set version="$VERSION"
|
||||
echo "package.json version: $(npm pkg get version)"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
# 安装 pnpm(用于 workspace 管理,版本由根 package.json packageManager 字段指定)
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
# Python 3.12+ 移除了 distutils,node-gyp(better-sqlite3 等 native 模块)依赖它;安装 setuptools 以提供 distutils
|
||||
- name: Install setuptools for node-gyp (Python 3.12+)
|
||||
shell: bash
|
||||
run: python3 -m pip install setuptools --break-system-packages 2>/dev/null || python3 -m pip install setuptools
|
||||
|
||||
# Linux 构建 deb/rpm 包需要 rpm 工具;arm64 runner 需要系统 fpm(内置 fpm 仅 x86)
|
||||
- name: Install Linux build tools
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm ruby ruby-dev
|
||||
sudo gem install fpm
|
||||
echo "USE_SYSTEM_FPM=true" >> "$GITHUB_ENV"
|
||||
|
||||
# pnpm install 会建立 workspace 链接,qiming-mcp-stdio-proxy 通过 prepare 脚本自动构建
|
||||
- name: Install workspace dependencies
|
||||
run: pnpm install --filter @qiming-ai/qimingclaw...
|
||||
|
||||
# 为 Electron 内嵌 Node 重编 better-sqlite3,避免 Linux arm64 等平台运行时 "cannot open shared object file"
|
||||
- name: Rebuild native modules for Electron
|
||||
working-directory: crates/agent-electron-client
|
||||
run: npm run electron-rebuild
|
||||
|
||||
# 缓存内置 Node.js(所有平台)、Git(仅 Windows)、uv(所有平台)和 qimingcode(所有平台)
|
||||
- name: Cache bundled Node.js
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/node
|
||||
key: bundled-node-24-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('crates/agent-electron-client/package.json') }}
|
||||
|
||||
- name: Cache bundled Git (Windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/git
|
||||
key: bundled-git-2.47.1-windows-${{ hashFiles('crates/agent-electron-client/package.json') }}
|
||||
|
||||
- name: Cache uv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/uv
|
||||
key: bundled-uv-v2-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('crates/agent-electron-client/scripts/prepare/prepare-uv.js') }}
|
||||
|
||||
- name: Cache qimingcode
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/qimingcode
|
||||
key: bundled-qimingcode-v1-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('crates/agent-electron-client/scripts/prepare/prepare-qimingcode.js') }}
|
||||
|
||||
- name: Prepare bundled resources (uv, node, git, qimingcode)
|
||||
working-directory: crates/agent-electron-client
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.arch }}
|
||||
run: npm run prepare:all
|
||||
timeout-minutes: 15
|
||||
|
||||
# ---- macOS:导入证书并配置公证(可选) ----
|
||||
- name: Import Apple certificate (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
if [ -z "${APPLE_CERTIFICATE:-}" ]; then
|
||||
echo "APPLE_CERTIFICATE not set, signing will be skipped"
|
||||
exit 0
|
||||
fi
|
||||
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
KEYCHAIN_PASSWORD=actions
|
||||
echo -n "$APPLE_CERTIFICATE" | base64 --decode -o "$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" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
|
||||
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
||||
rm -f "$CERTIFICATE_PATH"
|
||||
echo "CSC_KEYCHAIN=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prepare Apple notarization key (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
|
||||
run: |
|
||||
if [ -z "${APPLE_API_KEY:-}" ] || [ -z "${APPLE_API_KEY_ID:-}" ]; then
|
||||
echo "Apple notarization secrets not set, notarization will be skipped"
|
||||
exit 0
|
||||
fi
|
||||
if [ -z "${APPLE_ISSUER_ID:-}" ]; then
|
||||
echo "APPLE_ISSUER_ID required for notarization"
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p "$RUNNER_TEMP/apple-api-key"
|
||||
echo -n "$APPLE_API_KEY" | base64 --decode -o "$RUNNER_TEMP/apple-api-key/AuthKey.p8"
|
||||
echo "APPLE_API_KEY=$RUNNER_TEMP/apple-api-key/AuthKey.p8" >> "$GITHUB_ENV"
|
||||
echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV"
|
||||
echo "APPLE_API_ISSUER=$APPLE_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
echo "APPLE_ISSUER_ID=$APPLE_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
|
||||
# 若用户看到「已损坏」:请确认已配置 APPLE_CERTIFICATE / APPLE_SIGNING_IDENTITY 及公证用 APPLE_API_KEY 等,且构建日志中有签名与公证成功
|
||||
# Windows 签名已废弃,改为本地手签方案
|
||||
- name: Clean previous build artifacts
|
||||
shell: bash
|
||||
working-directory: crates/agent-electron-client
|
||||
run: |
|
||||
rm -rf release node_modules/.cache
|
||||
echo "Cleaned release and cache directories"
|
||||
|
||||
- name: Build Electron app
|
||||
shell: bash
|
||||
working-directory: crates/agent-electron-client
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.arch }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_NAME: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
run: |
|
||||
echo "Building Electron app for ${{ runner.os }} (${{ matrix.arch }})"
|
||||
npm run ${{ matrix.dist_cmd }} -- --publish never
|
||||
|
||||
- name: Upload artifacts to Release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
OUT_DIR="crates/agent-electron-client/release/${VERSION}"
|
||||
if [ ! -d "$OUT_DIR" ]; then
|
||||
echo "::error::Output directory not found: $OUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
for f in "${OUT_DIR}"/*; do
|
||||
[ -e "$f" ] || continue
|
||||
[ -f "$f" ] || continue
|
||||
echo "Uploading $(basename "$f") ..."
|
||||
gh release upload "$TAG" "$f" --clobber --repo "$GITHUB_REPOSITORY"
|
||||
done
|
||||
|
||||
# ==================== 同步到 MinIO S3(构建产物)和阿里云 OSS(latest.json) ====================
|
||||
# prerelease-v* tag 推送触发后,构建完成即自动同步到 MinIO S3 并更新 beta/latest.json
|
||||
sync-to-minio:
|
||||
name: Sync prerelease to MinIO S3 (beta)
|
||||
needs: build-electron
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
env:
|
||||
# MinIO S3 配置(存放构建产物和 yml 文件)
|
||||
S3_ENDPOINT: "https://s3.qiming.com:9443"
|
||||
S3_REGION: "us-east-1"
|
||||
S3_BUCKET: "qimingclaw"
|
||||
S3_CDN_BASE: "https://s3.qiming.com:9443/qimingclaw"
|
||||
# 阿里云 OSS 配置(仅存放 latest.json)
|
||||
OSS_ENDPOINT: "https://oss-rg-china-mainland.aliyuncs.com"
|
||||
OSS_REGION: "rg-china-mainland"
|
||||
OSS_BUCKET: "oss://qiming-packages"
|
||||
OSS_CDN_BASE: "https://qiming-packages.oss-rg-china-mainland.aliyuncs.com"
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
- name: Ensure awscli v2 (for S3)
|
||||
run: |
|
||||
if aws --version 2>/dev/null; then
|
||||
echo "AWS CLI already installed"
|
||||
else
|
||||
curl -fSL -o /tmp/awscliv2.zip \
|
||||
"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
|
||||
unzip -q /tmp/awscliv2.zip -d /tmp
|
||||
sudo /tmp/aws/install --bin-dir /usr/local/bin
|
||||
fi
|
||||
aws --version
|
||||
|
||||
- name: Install ossutil v2 (for latest.json)
|
||||
run: |
|
||||
curl -fSL -o /tmp/ossutil.zip \
|
||||
"https://gosspublic.alicdn.com/ossutil/v2/2.2.0/ossutil-2.2.0-linux-amd64.zip"
|
||||
unzip -q /tmp/ossutil.zip -d /tmp
|
||||
sudo mv /tmp/ossutil-2.2.0-linux-amd64/ossutil /usr/local/bin/
|
||||
ossutil version
|
||||
|
||||
- name: Configure AWS credentials for S3
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"
|
||||
aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY"
|
||||
aws configure set default.region "$S3_REGION"
|
||||
aws configure set default.s3.addressing_style path
|
||||
aws configure set endpoint_url "$S3_ENDPOINT"
|
||||
|
||||
- name: Download all release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
mkdir -p /tmp/release-assets
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir /tmp/release-assets
|
||||
echo "==> Downloaded assets:"
|
||||
ls -lh /tmp/release-assets/
|
||||
|
||||
- name: Generate latest.json and platform yml from assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
VERSION="${TAG#prerelease-v}"
|
||||
S3_URL_PREFIX="${S3_CDN_BASE}/qimingclaw-electron/beta-build/${TAG}"
|
||||
ASSETS_DIR="/tmp/release-assets"
|
||||
|
||||
# semver MVP guard: only allow numeric x.y.z
|
||||
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::Invalid version '${VERSION}'. MVP only supports numeric x.y.z"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 删除 GitHub 自带的 yml,仅保留由 latest.json 派生的 yml
|
||||
rm -f "${ASSETS_DIR}"/*.yml
|
||||
|
||||
# 获取 release notes 与 pub_date
|
||||
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body,createdAt 2>/dev/null || echo '{}')
|
||||
NOTES=$(echo "$RELEASE_JSON" | jq -r '.body // "QimingClaw 功能优化和问题修复。建议更新到最新版本以获得更好的体验。"' | head -c 500)
|
||||
PUB_DATE=$(echo "$RELEASE_JSON" | jq -r '.createdAt // empty')
|
||||
if [ -z "$PUB_DATE" ]; then
|
||||
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||||
fi
|
||||
|
||||
add_platform() {
|
||||
local key="$1" file="$2"
|
||||
local filepath="${ASSETS_DIR}/${file}"
|
||||
if [ -f "$filepath" ]; then
|
||||
local sig size
|
||||
sig=$(sha512sum "$filepath" | awk '{print $1}' | xxd -r -p | base64 -w0)
|
||||
if [ -z "$sig" ]; then
|
||||
echo " Skipped: ${key} -> ${file} (signature generation failed)"
|
||||
return 0
|
||||
fi
|
||||
size=$(stat -c%s "$filepath" 2>/dev/null || stat -f%z "$filepath")
|
||||
PLATFORMS=$(echo "$PLATFORMS" | jq \
|
||||
--arg k "$key" \
|
||||
--arg u "${S3_URL_PREFIX}/${file}" \
|
||||
--arg s "$sig" \
|
||||
--argjson sz "$size" \
|
||||
'. + {($k): {"url": $u, "signature": $s, "size": $sz}}')
|
||||
echo " Added: ${key} -> ${file} (${size} bytes)"
|
||||
fi
|
||||
}
|
||||
|
||||
PLATFORMS="{}"
|
||||
echo "==> Scanning release assets for platforms..."
|
||||
add_platform "darwin-aarch64" "QimingClaw-${VERSION}-arm64.dmg"
|
||||
add_platform "darwin-aarch64-zip" "QimingClaw-${VERSION}-arm64-mac.zip"
|
||||
add_platform "darwin-x86_64" "QimingClaw-${VERSION}.dmg"
|
||||
add_platform "darwin-x86_64-zip" "QimingClaw-${VERSION}-mac.zip"
|
||||
add_platform "windows-x86_64" "QimingClaw-Setup-${VERSION}-unsigned.exe"
|
||||
add_platform "windows-x86_64-nsis" "QimingClaw-Setup-${VERSION}-unsigned.exe"
|
||||
add_platform "windows-x86_64-msi" "QimingClaw-${VERSION}-unsigned.msi"
|
||||
add_platform "linux-x86_64" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-appimage" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-deb" "QimingClaw-${VERSION}-amd64.deb"
|
||||
add_platform "linux-x86_64-rpm" "QimingClaw-${VERSION}-x86_64.rpm"
|
||||
add_platform "linux-aarch64" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-appimage" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-deb" "QimingClaw-${VERSION}-arm64.deb"
|
||||
add_platform "linux-aarch64-rpm" "QimingClaw-${VERSION}-aarch64.rpm"
|
||||
|
||||
if [ "$(echo "$PLATFORMS" | jq 'length')" = "0" ]; then
|
||||
echo "::warning::No exact filename matches, falling back to fuzzy scan"
|
||||
for file in "${ASSETS_DIR}"/*; do
|
||||
filename=$(basename "$file")
|
||||
case "$filename" in
|
||||
*.dmg|*.zip|*.exe|*.msi|*.AppImage|*.deb|*.rpm)
|
||||
key=""
|
||||
case "$filename" in
|
||||
*arm64*mac*|*arm64*.dmg) key="darwin-aarch64" ;;
|
||||
*x64*mac*|*x64*.dmg) key="darwin-x86_64" ;;
|
||||
*Setup*.exe|*setup*.exe) key="windows-x86_64" ;;
|
||||
*.msi) key="windows-x86_64-msi" ;;
|
||||
*arm64*.AppImage) key="linux-aarch64" ;;
|
||||
*amd64*.AppImage|*x64*.AppImage|*.AppImage) key="linux-x86_64" ;;
|
||||
*arm64*.deb) key="linux-aarch64-deb" ;;
|
||||
*amd64*.deb|*x64*.deb) key="linux-x86_64-deb" ;;
|
||||
*aarch64*.rpm) key="linux-aarch64-rpm" ;;
|
||||
*x86_64*.rpm|*x64*.rpm) key="linux-x86_64-rpm" ;;
|
||||
esac
|
||||
[ -n "$key" ] && add_platform "$key" "$filename"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
gen_yml() {
|
||||
local channel="$1"
|
||||
local keys="$2"
|
||||
local first_key first_file
|
||||
first_key=$(echo "$PLATFORMS" | jq -r --argjson keys "$keys" 'limit(1; $keys[] as $k | select(.[$k] != null) | $k)')
|
||||
[ -z "$first_key" ] || [ "$first_key" = "null" ] && return 0
|
||||
first_file=$(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].url' | sed 's|.*/||')
|
||||
{
|
||||
echo "version: $VERSION"
|
||||
echo "path: $first_file"
|
||||
echo "sha512: $(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].signature')"
|
||||
echo "releaseDate: $PUB_DATE"
|
||||
echo "files:"
|
||||
echo "$PLATFORMS" | jq -r --argjson keys "$keys" '
|
||||
[ $keys[] as $k | select(.[$k] != null) | .[$k] |
|
||||
" - url: " + (.url | split("/") | last) + "\n sha512: " + .signature + "\n size: " + (.size | tostring)
|
||||
] | join("\n")
|
||||
'
|
||||
} > "${ASSETS_DIR}/${channel}.yml"
|
||||
echo " Generated ${channel}.yml (primary: $first_file)"
|
||||
}
|
||||
DARWIN_KEYS='["darwin-aarch64-zip","darwin-x86_64-zip","darwin-aarch64","darwin-x86_64"]'
|
||||
LINUX_KEYS='["linux-x86_64-appimage","linux-aarch64-appimage","linux-x86_64","linux-aarch64","linux-x86_64-deb","linux-aarch64-deb","linux-x86_64-rpm","linux-aarch64-rpm"]'
|
||||
LINUX_ARM64_KEYS='["linux-aarch64-appimage","linux-aarch64","linux-aarch64-deb","linux-aarch64-rpm"]'
|
||||
LINUX_X64_KEYS='["linux-x86_64-appimage","linux-x86_64","linux-x86_64-deb","linux-x86_64-rpm"]'
|
||||
WIN_KEYS='["windows-x86_64","windows-x86_64-nsis","windows-x86_64-msi"]'
|
||||
gen_yml "latest-mac" "$DARWIN_KEYS"
|
||||
gen_yml "latest-linux" "$LINUX_KEYS"
|
||||
gen_yml "latest-linux-arm64" "$LINUX_ARM64_KEYS"
|
||||
gen_yml "latest-linux-x64" "$LINUX_X64_KEYS"
|
||||
gen_yml "latest" "$WIN_KEYS"
|
||||
echo "==> Platform yml written to ${ASSETS_DIR}"
|
||||
|
||||
# 构建 yml 文件的完整 S3 URL(供客户端 electron-updater 直接使用)
|
||||
YML_URLS=$(jq -n \
|
||||
--arg darwin "${S3_URL_PREFIX}/latest-mac.yml" \
|
||||
--arg linux "${S3_URL_PREFIX}/latest-linux.yml" \
|
||||
--arg win "${S3_URL_PREFIX}/latest.yml" \
|
||||
'{darwin: $darwin, linux: $linux, win: $win}')
|
||||
|
||||
LATEST_JSON=$(jq -n \
|
||||
--arg version "$VERSION" \
|
||||
--arg notes "$NOTES" \
|
||||
--arg pub_date "$PUB_DATE" \
|
||||
--argjson platforms "$PLATFORMS" \
|
||||
--argjson yml "$YML_URLS" \
|
||||
'{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms, yml: $yml}')
|
||||
echo "$LATEST_JSON" > /tmp/latest.json
|
||||
echo "==> Generated latest.json"
|
||||
echo "$LATEST_JSON" | jq .
|
||||
|
||||
- name: Upload all assets to MinIO S3
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
S3_PREFIX="qimingclaw-electron/beta-build/${TAG}"
|
||||
|
||||
for file in /tmp/release-assets/*; do
|
||||
filename=$(basename "$file")
|
||||
echo "Uploading to S3: ${filename}"
|
||||
aws s3 cp "$file" "s3://${S3_BUCKET}/${S3_PREFIX}/${filename}" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
done
|
||||
|
||||
# 同步 latest.json 到 S3(版本化路径 + beta 滚动指针)
|
||||
echo "Uploading latest.json to S3..."
|
||||
aws s3 cp /tmp/latest.json "s3://${S3_BUCKET}/${S3_PREFIX}/latest.json" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
aws s3 cp /tmp/latest.json "s3://${S3_BUCKET}/qimingclaw-electron/beta/latest.json" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
|
||||
- name: Verify S3 upload
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
VERIFY_URL="${S3_CDN_BASE}/qimingclaw-electron/beta-build/${TAG}/latest-mac.yml"
|
||||
HTTP_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" "$VERIFY_URL")
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "==> S3 upload verified: latest-mac.yml (HTTP ${HTTP_STATUS})"
|
||||
else
|
||||
echo "::error::S3 upload verification failed: latest-mac.yml (HTTP ${HTTP_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload latest.json to Alibaba OSS (beta channel)
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
echo "==> Publishing latest.json to beta channel on OSS"
|
||||
# 写入版本化路径
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/beta-build/${TAG}/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
# 写入 beta/latest.json(供 beta 通道用户检查更新)
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/beta/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
echo "==> Uploaded latest.json to OSS beta/latest.json (stable pointer untouched)"
|
||||
|
||||
- name: Verify OSS upload
|
||||
run: |
|
||||
VERIFY_URL="${OSS_CDN_BASE}/qimingclaw-electron/beta/latest.json"
|
||||
HTTP_STATUS=$(curl -sS -o /tmp/oss-latest.json -w "%{http_code}" "$VERIFY_URL")
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "==> beta/latest.json: OK (HTTP ${HTTP_STATUS})"
|
||||
cat /tmp/oss-latest.json | jq .
|
||||
else
|
||||
echo "::error::beta/latest.json not found on OSS (HTTP ${HTTP_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
583
qimingclaw/.github/workflows/release-electron.yml
vendored
Normal file
583
qimingclaw/.github/workflows/release-electron.yml
vendored
Normal file
@@ -0,0 +1,583 @@
|
||||
# Electron 桌面客户端 — 正式版本发布
|
||||
# 与 Tauri 客户端完全分开:使用独立 tag 与独立 GitHub Release,不共用 v* tag。
|
||||
#
|
||||
# 触发:推送 tag electron-v* → 构建并创建 Release。同步请用 workflow_dispatch 手动触发。
|
||||
#
|
||||
# 支持平台:
|
||||
# - macOS arm64(macos-latest)
|
||||
# - macOS x64(macos-latest, cross-compile)
|
||||
# - Windows x64(msi)
|
||||
# - Linux x64(AppImage + deb)
|
||||
# - Linux arm64(AppImage + deb)
|
||||
#
|
||||
# 使用的 Secrets(可与 Tauri 共用 Apple 相关,也可单独配置):
|
||||
# - GH_PAT(可选):创建 Release、上传资产(未配置时自动回退到 github.token,需 Contents: Write)
|
||||
# - macOS 签名/公证(未配置时 Mac 构建为无签名包,用户打开会提示「已损坏」):
|
||||
# - APPLE_TEAM_ID / APPLE_SIGNING_IDENTITY / APPLE_CERTIFICATE(.p12 Base64)/ APPLE_CERTIFICATE_PASSWORD
|
||||
# - 公证:APPLE_API_KEY(.p8 Base64)/ APPLE_API_KEY_ID / APPLE_ISSUER_ID
|
||||
# - Windows:本 workflow 仅构建产物,不在 CI 内签名;签名在本地 Windows(SimplySign)手动完成
|
||||
# 详见 crates/agent-electron-client/docs/windows-signing.md
|
||||
# - MINIO_ACCESS_KEY_ID / MINIO_SECRET_ACCESS_KEY:上传到 MinIO S3
|
||||
# - OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET:上传 latest.json 到阿里云 OSS
|
||||
#
|
||||
# 产物目录:crates/agent-electron-client/release/${version}/(由 package.json directories.output 决定)
|
||||
|
||||
name: Release Electron App
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "electron-v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag to sync (e.g. electron-v0.9.0)"
|
||||
required: true
|
||||
type: string
|
||||
channel:
|
||||
description: "Update channel pointer to publish (stable|beta). beta will NOT overwrite latest/latest.json"
|
||||
required: false
|
||||
default: stable
|
||||
type: choice
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare release
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#electron-v}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Electron release version: $VERSION"
|
||||
|
||||
- name: Read release notes
|
||||
id: notes
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TAG="${{ github.ref_name }}"
|
||||
NOTES_FILE="release-notes/${TAG}.md"
|
||||
if [ -f "$NOTES_FILE" ]; then
|
||||
echo "Using release notes from $NOTES_FILE"
|
||||
cp "$NOTES_FILE" release_notes.txt
|
||||
else
|
||||
echo "QimingClaw ${VERSION}。请在 Assets 中下载对应平台安装包。" > release_notes.txt
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" 2>/dev/null; then
|
||||
echo "Release $TAG already exists"
|
||||
else
|
||||
gh release create "$TAG" \
|
||||
--title "QimingClaw (Electron) ${{ steps.version.outputs.version }}" \
|
||||
--notes-file release_notes.txt \
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
echo "Created release $TAG"
|
||||
fi
|
||||
|
||||
build-electron:
|
||||
name: Build Electron (${{ matrix.platform }} ${{ matrix.arch }})
|
||||
needs: prepare
|
||||
if: github.event_name == 'push'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: macos-latest
|
||||
arch: arm64
|
||||
dist_cmd: dist:mac:arm64
|
||||
- platform: macos-latest
|
||||
arch: x64
|
||||
dist_cmd: dist:mac:x64
|
||||
- platform: windows-latest
|
||||
arch: x64
|
||||
dist_cmd: dist:win
|
||||
- platform: ubuntu-24.04
|
||||
arch: x64
|
||||
dist_cmd: dist:linux:x64
|
||||
|
||||
- platform: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
dist_cmd: dist:linux:arm64
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set version in package.json
|
||||
shell: bash
|
||||
working-directory: crates/agent-electron-client
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
npm pkg set version="$VERSION"
|
||||
echo "package.json version: $(npm pkg get version)"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
# 安装 pnpm(用于 workspace 管理,版本由根 package.json packageManager 字段指定)
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
# Python 3.12+ 移除了 distutils,node-gyp(better-sqlite3 等 native 模块)依赖它;安装 setuptools 以提供 distutils
|
||||
- name: Install setuptools for node-gyp (Python 3.12+)
|
||||
shell: bash
|
||||
run: python3 -m pip install setuptools --break-system-packages 2>/dev/null || python3 -m pip install setuptools
|
||||
|
||||
# Linux 构建 deb/rpm 包需要 rpm 工具;arm64 runner 需要系统 fpm(内置 fpm 仅 x86)
|
||||
- name: Install Linux build tools
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm ruby ruby-dev
|
||||
sudo gem install fpm
|
||||
echo "USE_SYSTEM_FPM=true" >> "$GITHUB_ENV"
|
||||
|
||||
# pnpm install 会建立 workspace 链接,qiming-mcp-stdio-proxy 通过 prepare 脚本自动构建
|
||||
- name: Install workspace dependencies
|
||||
run: pnpm install --filter @qiming-ai/qimingclaw...
|
||||
|
||||
# 为 Electron 内嵌 Node 重编 better-sqlite3,避免 Linux arm64 等平台运行时 "cannot open shared object file"
|
||||
- name: Rebuild native modules for Electron
|
||||
working-directory: crates/agent-electron-client
|
||||
run: npm run electron-rebuild
|
||||
|
||||
# 缓存内置 Node.js(所有平台)、Git(仅 Windows)、uv(所有平台)和 qimingcode(所有平台)
|
||||
- name: Cache bundled Node.js
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/node
|
||||
key: bundled-node-24-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('crates/agent-electron-client/package.json') }}
|
||||
|
||||
- name: Cache bundled Git (Windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/git
|
||||
key: bundled-git-2.47.1-windows-${{ hashFiles('crates/agent-electron-client/package.json') }}
|
||||
|
||||
- name: Cache uv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/uv
|
||||
key: bundled-uv-v2-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('crates/agent-electron-client/scripts/prepare/prepare-uv.js') }}
|
||||
|
||||
- name: Cache qimingcode
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/qimingcode
|
||||
key: bundled-qimingcode-v1-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('crates/agent-electron-client/scripts/prepare/prepare-qimingcode.js') }}
|
||||
|
||||
- name: Prepare bundled resources (uv, node, git, qimingcode)
|
||||
working-directory: crates/agent-electron-client
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.arch }}
|
||||
run: npm run prepare:all
|
||||
timeout-minutes: 15
|
||||
|
||||
# ---- macOS:导入证书并配置公证(可选) ----
|
||||
- name: Import Apple certificate (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
if [ -z "${APPLE_CERTIFICATE:-}" ]; then
|
||||
echo "APPLE_CERTIFICATE not set, signing will be skipped"
|
||||
exit 0
|
||||
fi
|
||||
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
KEYCHAIN_PASSWORD=actions
|
||||
echo -n "$APPLE_CERTIFICATE" | base64 --decode -o "$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" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
|
||||
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
||||
rm -f "$CERTIFICATE_PATH"
|
||||
echo "CSC_KEYCHAIN=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prepare Apple notarization key (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
|
||||
run: |
|
||||
if [ -z "${APPLE_API_KEY:-}" ] || [ -z "${APPLE_API_KEY_ID:-}" ]; then
|
||||
echo "Apple notarization secrets not set, notarization will be skipped"
|
||||
exit 0
|
||||
fi
|
||||
if [ -z "${APPLE_ISSUER_ID:-}" ]; then
|
||||
echo "APPLE_ISSUER_ID required for notarization"
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p "$RUNNER_TEMP/apple-api-key"
|
||||
echo -n "$APPLE_API_KEY" | base64 --decode -o "$RUNNER_TEMP/apple-api-key/AuthKey.p8"
|
||||
echo "APPLE_API_KEY=$RUNNER_TEMP/apple-api-key/AuthKey.p8" >> "$GITHUB_ENV"
|
||||
echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV"
|
||||
echo "APPLE_API_ISSUER=$APPLE_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
echo "APPLE_ISSUER_ID=$APPLE_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
|
||||
# 若用户看到「已损坏」:请确认已配置 APPLE_CERTIFICATE / APPLE_SIGNING_IDENTITY 及公证用 APPLE_API_KEY 等,且构建日志中有签名与公证成功
|
||||
# Windows 签名已废弃,改为本地手签方案
|
||||
- name: Clean previous build artifacts
|
||||
shell: bash
|
||||
working-directory: crates/agent-electron-client
|
||||
run: |
|
||||
rm -rf release node_modules/.cache
|
||||
echo "Cleaned release and cache directories"
|
||||
|
||||
- name: Build Electron app
|
||||
shell: bash
|
||||
working-directory: crates/agent-electron-client
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.arch }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_NAME: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
run: |
|
||||
if [ "${{ runner.os }}" = "macOS" ] && [ -z "${{ secrets.APPLE_CERTIFICATE }}" ]; then
|
||||
echo "No Apple certificate, building unsigned Mac package for ${{ matrix.arch }}"
|
||||
npm run dist:mac:unsigned:${{ matrix.arch }} -- --publish never
|
||||
elif [ "${{ runner.os }}" = "Windows" ]; then
|
||||
npm run ${{ matrix.dist_cmd }} -- --publish never
|
||||
else
|
||||
npm run ${{ matrix.dist_cmd }} -- --publish never
|
||||
fi
|
||||
|
||||
- name: Upload artifacts to Release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
OUT_DIR="crates/agent-electron-client/release/${VERSION}"
|
||||
if [ ! -d "$OUT_DIR" ]; then
|
||||
echo "::error::Output directory not found: $OUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
for f in "${OUT_DIR}"/*; do
|
||||
[ -e "$f" ] || continue
|
||||
[ -f "$f" ] || continue
|
||||
echo "Uploading $(basename "$f") ..."
|
||||
gh release upload "$TAG" "$f" --clobber --repo "$GITHUB_REPOSITORY"
|
||||
done
|
||||
|
||||
# ==================== 同步到 MinIO S3(构建产物)和阿里云 OSS(latest.json) ====================
|
||||
# 手动触发:workflow_dispatch 输入 tag(如 electron-v0.8.0)
|
||||
sync-to-minio:
|
||||
name: Sync release to MinIO S3 & OSS
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
env:
|
||||
# MinIO S3 配置(存放构建产物和 yml 文件)
|
||||
S3_ENDPOINT: "https://s3.qiming.com:9443"
|
||||
S3_REGION: "us-east-1"
|
||||
S3_BUCKET: "qimingclaw"
|
||||
S3_CDN_BASE: "https://s3.qiming.com:9443/qimingclaw"
|
||||
# 阿里云 OSS 配置(仅存放 latest.json)
|
||||
OSS_ENDPOINT: "https://oss-rg-china-mainland.aliyuncs.com"
|
||||
OSS_REGION: "rg-china-mainland"
|
||||
OSS_BUCKET: "oss://qiming-packages"
|
||||
OSS_CDN_BASE: "https://qiming-packages.oss-rg-china-mainland.aliyuncs.com"
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_CHANNEL: ${{ inputs.channel || 'stable' }}
|
||||
steps:
|
||||
- name: Ensure awscli v2 (for S3)
|
||||
run: |
|
||||
if aws --version 2>/dev/null; then
|
||||
echo "AWS CLI already installed"
|
||||
else
|
||||
curl -fSL -o /tmp/awscliv2.zip \
|
||||
"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
|
||||
unzip -q /tmp/awscliv2.zip -d /tmp
|
||||
sudo /tmp/aws/install --bin-dir /usr/local/bin
|
||||
fi
|
||||
aws --version
|
||||
|
||||
- name: Install ossutil v2 (for latest.json)
|
||||
run: |
|
||||
curl -fSL -o /tmp/ossutil.zip \
|
||||
"https://gosspublic.alicdn.com/ossutil/v2/2.2.0/ossutil-2.2.0-linux-amd64.zip"
|
||||
unzip -q /tmp/ossutil.zip -d /tmp
|
||||
sudo mv /tmp/ossutil-2.2.0-linux-amd64/ossutil /usr/local/bin/
|
||||
ossutil version
|
||||
|
||||
- name: Configure AWS credentials for S3
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"
|
||||
aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY"
|
||||
aws configure set default.region "$S3_REGION"
|
||||
aws configure set default.s3.addressing_style path
|
||||
aws configure set endpoint_url "$S3_ENDPOINT"
|
||||
|
||||
- name: Download all release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
mkdir -p /tmp/release-assets
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir /tmp/release-assets
|
||||
echo "==> Downloaded assets:"
|
||||
ls -lh /tmp/release-assets/
|
||||
|
||||
- name: Generate latest.json and platform yml from assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
VERSION="${TAG#electron-v}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
S3_URL_PREFIX="${S3_CDN_BASE}/qimingclaw-electron/${TAG}"
|
||||
ASSETS_DIR="/tmp/release-assets"
|
||||
|
||||
# semver MVP guard: only allow numeric x.y.z
|
||||
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::Invalid version '${VERSION}'. MVP only supports numeric x.y.z"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "beta" ]; then
|
||||
echo "::error::Invalid channel '${CHANNEL}', expected stable or beta"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 删除 GitHub 自带的 yml,仅保留由 latest.json 派生的 yml
|
||||
rm -f "${ASSETS_DIR}"/*.yml
|
||||
|
||||
# 获取 release notes 与 pub_date
|
||||
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body,createdAt 2>/dev/null || echo '{}')
|
||||
NOTES=$(echo "$RELEASE_JSON" | jq -r '.body // "QimingClaw 功能优化和问题修复。建议更新到最新版本以获得更好的体验。"' | head -c 500)
|
||||
PUB_DATE=$(echo "$RELEASE_JSON" | jq -r '.createdAt // empty')
|
||||
if [ -z "$PUB_DATE" ]; then
|
||||
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||||
fi
|
||||
|
||||
add_platform() {
|
||||
local key="$1" file="$2"
|
||||
local filepath="${ASSETS_DIR}/${file}"
|
||||
if [ -f "$filepath" ]; then
|
||||
local sig size
|
||||
sig=$(sha512sum "$filepath" | awk '{print $1}' | xxd -r -p | base64 -w0)
|
||||
if [ -z "$sig" ]; then
|
||||
echo " Skipped: ${key} -> ${file} (signature generation failed)"
|
||||
return 0
|
||||
fi
|
||||
size=$(stat -c%s "$filepath" 2>/dev/null || stat -f%z "$filepath")
|
||||
PLATFORMS=$(echo "$PLATFORMS" | jq \
|
||||
--arg k "$key" \
|
||||
--arg u "${S3_URL_PREFIX}/${file}" \
|
||||
--arg s "$sig" \
|
||||
--argjson sz "$size" \
|
||||
'. + {($k): {"url": $u, "signature": $s, "size": $sz}}')
|
||||
echo " Added: ${key} -> ${file} (${size} bytes)"
|
||||
fi
|
||||
}
|
||||
|
||||
PLATFORMS="{}"
|
||||
echo "==> Scanning release assets for platforms..."
|
||||
add_platform "darwin-aarch64" "QimingClaw-${VERSION}-arm64.dmg"
|
||||
add_platform "darwin-aarch64-zip" "QimingClaw-${VERSION}-arm64-mac.zip"
|
||||
add_platform "darwin-x86_64" "QimingClaw-${VERSION}.dmg"
|
||||
add_platform "darwin-x86_64-zip" "QimingClaw-${VERSION}-mac.zip"
|
||||
add_platform "windows-x86_64" "QimingClaw.Setup.${VERSION}.exe"
|
||||
add_platform "windows-x86_64-nsis" "QimingClaw.Setup.${VERSION}.exe"
|
||||
add_platform "windows-x86_64-msi" "QimingClaw.${VERSION}.msi"
|
||||
add_platform "linux-x86_64" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-appimage" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-deb" "QimingClaw-${VERSION}-amd64.deb"
|
||||
add_platform "linux-x86_64-rpm" "QimingClaw-${VERSION}-x86_64.rpm"
|
||||
add_platform "linux-aarch64" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-appimage" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-deb" "QimingClaw-${VERSION}-arm64.deb"
|
||||
add_platform "linux-aarch64-rpm" "QimingClaw-${VERSION}-aarch64.rpm"
|
||||
|
||||
if [ "$(echo "$PLATFORMS" | jq 'length')" = "0" ]; then
|
||||
echo "::warning::No exact filename matches, falling back to fuzzy scan"
|
||||
for file in "${ASSETS_DIR}"/*; do
|
||||
filename=$(basename "$file")
|
||||
case "$filename" in
|
||||
*.dmg|*.zip|*.exe|*.msi|*.AppImage|*.deb|*.rpm)
|
||||
key=""
|
||||
case "$filename" in
|
||||
*arm64*mac*|*arm64*.dmg) key="darwin-aarch64" ;;
|
||||
*x64*mac*|*x64*.dmg) key="darwin-x86_64" ;;
|
||||
*Setup*.exe|*setup*.exe) key="windows-x86_64" ;;
|
||||
*.msi) key="windows-x86_64-msi" ;;
|
||||
*arm64*.AppImage) key="linux-aarch64" ;;
|
||||
*amd64*.AppImage|*x64*.AppImage|*.AppImage) key="linux-x86_64" ;;
|
||||
*arm64*.deb) key="linux-aarch64-deb" ;;
|
||||
*amd64*.deb|*x64*.deb) key="linux-x86_64-deb" ;;
|
||||
*aarch64*.rpm) key="linux-aarch64-rpm" ;;
|
||||
*x86_64*.rpm|*x64*.rpm) key="linux-x86_64-rpm" ;;
|
||||
esac
|
||||
[ -n "$key" ] && add_platform "$key" "$filename"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
gen_yml() {
|
||||
local channel="$1"
|
||||
local keys="$2"
|
||||
local first_key first_file
|
||||
first_key=$(echo "$PLATFORMS" | jq -r --argjson keys "$keys" 'limit(1; $keys[] as $k | select(.[$k] != null) | $k)')
|
||||
[ -z "$first_key" ] || [ "$first_key" = "null" ] && return 0
|
||||
first_file=$(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].url' | sed 's|.*/||')
|
||||
{
|
||||
echo "version: $VERSION"
|
||||
echo "path: $first_file"
|
||||
echo "sha512: $(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].signature')"
|
||||
echo "releaseDate: $PUB_DATE"
|
||||
echo "files:"
|
||||
echo "$PLATFORMS" | jq -r --argjson keys "$keys" '
|
||||
[ $keys[] as $k | select(.[$k] != null) | .[$k] |
|
||||
" - url: " + (.url | split("/") | last) + "\n sha512: " + .signature + "\n size: " + (.size | tostring)
|
||||
] | join("\n")
|
||||
'
|
||||
} > "${ASSETS_DIR}/${channel}.yml"
|
||||
echo " Generated ${channel}.yml (primary: $first_file)"
|
||||
}
|
||||
DARWIN_KEYS='["darwin-aarch64-zip","darwin-x86_64-zip","darwin-aarch64","darwin-x86_64"]'
|
||||
LINUX_KEYS='["linux-x86_64-appimage","linux-aarch64-appimage","linux-x86_64","linux-aarch64","linux-x86_64-deb","linux-aarch64-deb","linux-x86_64-rpm","linux-aarch64-rpm"]'
|
||||
LINUX_ARM64_KEYS='["linux-aarch64-appimage","linux-aarch64","linux-aarch64-deb","linux-aarch64-rpm"]'
|
||||
LINUX_X64_KEYS='["linux-x86_64-appimage","linux-x86_64","linux-x86_64-deb","linux-x86_64-rpm"]'
|
||||
WIN_KEYS='["windows-x86_64","windows-x86_64-nsis","windows-x86_64-msi"]'
|
||||
gen_yml "latest-mac" "$DARWIN_KEYS"
|
||||
gen_yml "latest-linux" "$LINUX_KEYS"
|
||||
gen_yml "latest-linux-arm64" "$LINUX_ARM64_KEYS"
|
||||
gen_yml "latest-linux-x64" "$LINUX_X64_KEYS"
|
||||
gen_yml "latest" "$WIN_KEYS"
|
||||
echo "==> Platform yml written to ${ASSETS_DIR}"
|
||||
|
||||
# 构建 yml 文件的完整 S3 URL(供客户端 electron-updater 直接使用)
|
||||
YML_URLS=$(jq -n \
|
||||
--arg darwin "${S3_URL_PREFIX}/latest-mac.yml" \
|
||||
--arg linux "${S3_URL_PREFIX}/latest-linux.yml" \
|
||||
--arg win "${S3_URL_PREFIX}/latest.yml" \
|
||||
'{darwin: $darwin, linux: $linux, win: $win}')
|
||||
|
||||
LATEST_JSON=$(jq -n \
|
||||
--arg version "$VERSION" \
|
||||
--arg notes "$NOTES" \
|
||||
--arg pub_date "$PUB_DATE" \
|
||||
--argjson platforms "$PLATFORMS" \
|
||||
--argjson yml "$YML_URLS" \
|
||||
'{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms, yml: $yml}')
|
||||
echo "$LATEST_JSON" > /tmp/latest.json
|
||||
echo "==> Generated latest.json"
|
||||
echo "$LATEST_JSON" | jq .
|
||||
|
||||
- name: Upload all assets to MinIO S3
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
S3_PREFIX="qimingclaw-electron/${TAG}"
|
||||
|
||||
for file in /tmp/release-assets/*; do
|
||||
filename=$(basename "$file")
|
||||
echo "Uploading to S3: ${filename}"
|
||||
aws s3 cp "$file" "s3://${S3_BUCKET}/${S3_PREFIX}/${filename}" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
done
|
||||
|
||||
# 同步 latest.json 到 S3(版本化路径 + 通道滚动指针)
|
||||
echo "Uploading latest.json to S3..."
|
||||
aws s3 cp /tmp/latest.json "s3://${S3_BUCKET}/${S3_PREFIX}/latest.json" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
aws s3 cp /tmp/latest.json "s3://${S3_BUCKET}/qimingclaw-electron/latest/latest.json" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
else
|
||||
aws s3 cp /tmp/latest.json "s3://${S3_BUCKET}/qimingclaw-electron/beta/latest.json" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
fi
|
||||
|
||||
- name: Verify S3 upload
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
VERIFY_URL="${S3_CDN_BASE}/qimingclaw-electron/${TAG}/latest-mac.yml"
|
||||
HTTP_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" "$VERIFY_URL")
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "==> S3 upload verified: latest-mac.yml (HTTP ${HTTP_STATUS})"
|
||||
else
|
||||
echo "::error::S3 upload verification failed: latest-mac.yml (HTTP ${HTTP_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload latest.json to Alibaba OSS
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
echo "==> Publishing latest.json to OSS for channel: ${CHANNEL}"
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/${TAG}/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/latest/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
echo "==> Uploaded latest.json to stable pointer: latest/latest.json"
|
||||
else
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/beta/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
echo "==> Uploaded latest.json to beta pointer: beta/latest.json (stable pointer untouched)"
|
||||
fi
|
||||
|
||||
- name: Verify OSS upload
|
||||
run: |
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
VERIFY_URL="${OSS_CDN_BASE}/qimingclaw-electron/latest/latest.json"
|
||||
else
|
||||
VERIFY_URL="${OSS_CDN_BASE}/qimingclaw-electron/beta/latest.json"
|
||||
fi
|
||||
HTTP_STATUS=$(curl -sS -o /tmp/oss-latest.json -w "%{http_code}" "$VERIFY_URL")
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "==> latest.json (${CHANNEL}): OK (HTTP ${HTTP_STATUS})"
|
||||
cat /tmp/oss-latest.json | jq .
|
||||
else
|
||||
echo "::error::latest.json for channel '${CHANNEL}' not found on OSS (HTTP ${HTTP_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
530
qimingclaw/.github/workflows/release-electron.yml.bak
vendored
Normal file
530
qimingclaw/.github/workflows/release-electron.yml.bak
vendored
Normal file
@@ -0,0 +1,530 @@
|
||||
# Electron 桌面客户端 — 正式版本发布
|
||||
# 与 Tauri 客户端完全分开:使用独立 tag 与独立 GitHub Release,不共用 v* tag。
|
||||
#
|
||||
# 触发:推送 tag electron-v* → 构建并创建 Release。OSS 同步请用 sync-oss.sh。
|
||||
#
|
||||
# 支持平台:
|
||||
# - macOS arm64(macos-latest)
|
||||
# - macOS x64(macos-latest, cross-compile)
|
||||
# - Windows x64(msi)
|
||||
# - Linux x64(AppImage + deb)
|
||||
# - Linux arm64(AppImage + deb)
|
||||
#
|
||||
# 使用的 Secrets(可与 Tauri 共用 Apple 相关,也可单独配置):
|
||||
# - GH_PAT(可选):创建 Release、上传资产(未配置时自动回退到 github.token,需 Contents: Write)
|
||||
# - macOS 签名/公证(未配置时 Mac 构建为无签名包,用户打开会提示「已损坏」):
|
||||
# - APPLE_TEAM_ID / APPLE_SIGNING_IDENTITY / APPLE_CERTIFICATE(.p12 Base64)/ APPLE_CERTIFICATE_PASSWORD
|
||||
# - 公证:APPLE_API_KEY(.p8 Base64)/ APPLE_API_KEY_ID / APPLE_ISSUER_ID
|
||||
# - Windows:本 workflow 仅构建产物,不在 CI 内签名;签名在本地 Windows(SimplySign)手动完成
|
||||
# 详见 crates/agent-electron-client/docs/windows-signing.md
|
||||
#
|
||||
# 产物目录:crates/agent-electron-client/release/${version}/(由 package.json directories.output 决定)
|
||||
|
||||
name: Release Electron App
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "electron-v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag to sync (e.g. electron-v0.9.0)"
|
||||
required: true
|
||||
type: string
|
||||
channel:
|
||||
description: "Update channel pointer to publish (stable|beta). beta will NOT overwrite latest/latest.json"
|
||||
required: false
|
||||
default: stable
|
||||
type: choice
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare release
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#electron-v}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Electron release version: $VERSION"
|
||||
|
||||
- name: Read release notes
|
||||
id: notes
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TAG="${{ github.ref_name }}"
|
||||
NOTES_FILE="release-notes/${TAG}.md"
|
||||
if [ -f "$NOTES_FILE" ]; then
|
||||
echo "Using release notes from $NOTES_FILE"
|
||||
cp "$NOTES_FILE" release_notes.txt
|
||||
else
|
||||
echo "QimingClaw ${VERSION}。请在 Assets 中下载对应平台安装包。" > release_notes.txt
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" 2>/dev/null; then
|
||||
echo "Release $TAG already exists"
|
||||
else
|
||||
gh release create "$TAG" \
|
||||
--title "QimingClaw (Electron) ${{ steps.version.outputs.version }}" \
|
||||
--notes-file release_notes.txt \
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
echo "Created release $TAG"
|
||||
fi
|
||||
|
||||
build-electron:
|
||||
name: Build Electron (${{ matrix.platform }} ${{ matrix.arch }})
|
||||
needs: prepare
|
||||
if: github.event_name == 'push'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: macos-latest
|
||||
arch: arm64
|
||||
dist_cmd: dist:mac:arm64
|
||||
- platform: macos-latest
|
||||
arch: x64
|
||||
dist_cmd: dist:mac:x64
|
||||
- platform: windows-latest
|
||||
arch: x64
|
||||
dist_cmd: dist:win
|
||||
- platform: ubuntu-24.04
|
||||
arch: x64
|
||||
dist_cmd: dist:linux:x64
|
||||
|
||||
- platform: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
dist_cmd: dist:linux:arm64
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set version in package.json
|
||||
shell: bash
|
||||
working-directory: crates/agent-electron-client
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
npm pkg set version="$VERSION"
|
||||
echo "package.json version: $(npm pkg get version)"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
# 安装 pnpm(用于 workspace 管理,版本由根 package.json packageManager 字段指定)
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
# Python 3.12+ 移除了 distutils,node-gyp(better-sqlite3 等 native 模块)依赖它;安装 setuptools 以提供 distutils
|
||||
- name: Install setuptools for node-gyp (Python 3.12+)
|
||||
shell: bash
|
||||
run: python3 -m pip install setuptools --break-system-packages 2>/dev/null || python3 -m pip install setuptools
|
||||
|
||||
# Linux 构建 deb/rpm 包需要 rpm 工具;arm64 runner 需要系统 fpm(内置 fpm 仅 x86)
|
||||
- name: Install Linux build tools
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm ruby ruby-dev
|
||||
sudo gem install fpm
|
||||
echo "USE_SYSTEM_FPM=true" >> "$GITHUB_ENV"
|
||||
|
||||
# pnpm install 会建立 workspace 链接,qiming-mcp-stdio-proxy 通过 prepare 脚本自动构建
|
||||
- name: Install workspace dependencies
|
||||
run: pnpm install --filter @qiming-ai/qimingclaw...
|
||||
|
||||
# 为 Electron 内嵌 Node 重编 better-sqlite3,避免 Linux arm64 等平台运行时 "cannot open shared object file"
|
||||
- name: Rebuild native modules for Electron
|
||||
working-directory: crates/agent-electron-client
|
||||
run: npm run electron-rebuild
|
||||
|
||||
# 缓存内置 Node.js(所有平台)、Git(仅 Windows)、uv(所有平台)和 qimingcode(所有平台)
|
||||
- name: Cache bundled Node.js
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/node
|
||||
key: bundled-node-24-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('crates/agent-electron-client/package.json') }}
|
||||
|
||||
- name: Cache bundled Git (Windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/git
|
||||
key: bundled-git-2.47.1-windows-${{ hashFiles('crates/agent-electron-client/package.json') }}
|
||||
|
||||
- name: Cache uv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/uv
|
||||
key: bundled-uv-v2-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('crates/agent-electron-client/scripts/prepare/prepare-uv.js') }}
|
||||
|
||||
- name: Cache qimingcode
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: crates/agent-electron-client/resources/qimingcode
|
||||
key: bundled-qimingcode-v1-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('crates/agent-electron-client/scripts/prepare/prepare-qimingcode.js') }}
|
||||
|
||||
- name: Prepare bundled resources (uv, node, git, qimingcode)
|
||||
working-directory: crates/agent-electron-client
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.arch }}
|
||||
run: npm run prepare:all
|
||||
timeout-minutes: 15
|
||||
|
||||
# ---- macOS:导入证书并配置公证(可选) ----
|
||||
- name: Import Apple certificate (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
if [ -z "${APPLE_CERTIFICATE:-}" ]; then
|
||||
echo "APPLE_CERTIFICATE not set, signing will be skipped"
|
||||
exit 0
|
||||
fi
|
||||
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
KEYCHAIN_PASSWORD=actions
|
||||
echo -n "$APPLE_CERTIFICATE" | base64 --decode -o "$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" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
|
||||
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
||||
rm -f "$CERTIFICATE_PATH"
|
||||
echo "CSC_KEYCHAIN=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prepare Apple notarization key (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
|
||||
run: |
|
||||
if [ -z "${APPLE_API_KEY:-}" ] || [ -z "${APPLE_API_KEY_ID:-}" ]; then
|
||||
echo "Apple notarization secrets not set, notarization will be skipped"
|
||||
exit 0
|
||||
fi
|
||||
if [ -z "${APPLE_ISSUER_ID:-}" ]; then
|
||||
echo "APPLE_ISSUER_ID required for notarization"
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p "$RUNNER_TEMP/apple-api-key"
|
||||
echo -n "$APPLE_API_KEY" | base64 --decode -o "$RUNNER_TEMP/apple-api-key/AuthKey.p8"
|
||||
echo "APPLE_API_KEY=$RUNNER_TEMP/apple-api-key/AuthKey.p8" >> "$GITHUB_ENV"
|
||||
echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV"
|
||||
echo "APPLE_API_ISSUER=$APPLE_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
echo "APPLE_ISSUER_ID=$APPLE_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
|
||||
# 若用户看到「已损坏」:请确认已配置 APPLE_CERTIFICATE / APPLE_SIGNING_IDENTITY 及公证用 APPLE_API_KEY 等,且构建日志中有签名与公证成功
|
||||
# Windows 签名已废弃,改为本地手签方案
|
||||
- name: Clean previous build artifacts
|
||||
shell: bash
|
||||
working-directory: crates/agent-electron-client
|
||||
run: |
|
||||
rm -rf release node_modules/.cache
|
||||
echo "Cleaned release and cache directories"
|
||||
|
||||
- name: Build Electron app
|
||||
shell: bash
|
||||
working-directory: crates/agent-electron-client
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.arch }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_NAME: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
run: |
|
||||
if [ "${{ runner.os }}" = "macOS" ] && [ -z "${{ secrets.APPLE_CERTIFICATE }}" ]; then
|
||||
echo "No Apple certificate, building unsigned Mac package for ${{ matrix.arch }}"
|
||||
npm run dist:mac:unsigned:${{ matrix.arch }} -- --publish never
|
||||
elif [ "${{ runner.os }}" = "Windows" ]; then
|
||||
npm run ${{ matrix.dist_cmd }} -- --publish never
|
||||
else
|
||||
npm run ${{ matrix.dist_cmd }} -- --publish never
|
||||
fi
|
||||
|
||||
- name: Upload artifacts to Release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
OUT_DIR="crates/agent-electron-client/release/${VERSION}"
|
||||
if [ ! -d "$OUT_DIR" ]; then
|
||||
echo "::error::Output directory not found: $OUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
for f in "${OUT_DIR}"/*; do
|
||||
[ -e "$f" ] || continue
|
||||
[ -f "$f" ] || continue
|
||||
echo "Uploading $(basename "$f") ..."
|
||||
gh release upload "$TAG" "$f" --clobber --repo "$GITHUB_REPOSITORY"
|
||||
done
|
||||
|
||||
# ==================== 同步到阿里云 OSS ====================
|
||||
# 手动触发:workflow_dispatch 输入 tag(如 electron-v0.8.0)
|
||||
sync-to-oss:
|
||||
name: Sync release assets to Alibaba Cloud OSS
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
env:
|
||||
OSS_ENDPOINT: "https://oss-rg-china-mainland.aliyuncs.com"
|
||||
OSS_REGION: "rg-china-mainland"
|
||||
OSS_BUCKET: "oss://qiming-packages"
|
||||
OSS_CDN_BASE: "https://qiming-packages.oss-rg-china-mainland.aliyuncs.com"
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_CHANNEL: ${{ inputs.channel || 'stable' }}
|
||||
steps:
|
||||
- name: Install ossutil v2
|
||||
run: |
|
||||
curl -fSL -o /tmp/ossutil.zip \
|
||||
"https://gosspublic.alicdn.com/ossutil/v2/2.2.0/ossutil-2.2.0-linux-amd64.zip"
|
||||
unzip -q /tmp/ossutil.zip -d /tmp
|
||||
sudo mv /tmp/ossutil-2.2.0-linux-amd64/ossutil /usr/local/bin/
|
||||
ossutil version
|
||||
|
||||
- name: Download all release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
mkdir -p /tmp/release-assets
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir /tmp/release-assets
|
||||
echo "==> Downloaded assets:"
|
||||
ls -lh /tmp/release-assets/
|
||||
|
||||
# 以 latest.json 为唯一数据源:生成 latest.json,并据此生成 electron-updater 所需的 yml(与 macOS 一致,Linux 也走 latest/latest.json):
|
||||
# latest-mac.yml、latest-linux.yml、latest-linux-arm64.yml、latest-linux-x64.yml、latest.yml;写入 release-assets 后一并上传。
|
||||
- name: Generate latest.json and platform yml from assets
|
||||
env:
|
||||
OSS_CDN_BASE: "https://qiming-packages.oss-rg-china-mainland.aliyuncs.com"
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
VERSION="${TAG#electron-v}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
OSS_URL_PREFIX="${OSS_CDN_BASE}/qimingclaw-electron/${TAG}"
|
||||
ASSETS_DIR="/tmp/release-assets"
|
||||
|
||||
# semver MVP guard: only allow numeric x.y.z(用 grep -E,不依赖 ripgrep,与 ubuntu-latest 默认环境一致)
|
||||
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::Invalid version '${VERSION}'. MVP only supports numeric x.y.z"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "beta" ]; then
|
||||
echo "::error::Invalid channel '${CHANNEL}', expected stable or beta"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 删除 GitHub 自带的 yml,仅保留由 latest.json 派生的 yml
|
||||
rm -f "${ASSETS_DIR}"/*.yml
|
||||
|
||||
# 获取 release notes 与 pub_date
|
||||
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body,createdAt 2>/dev/null || echo '{}')
|
||||
NOTES=$(echo "$RELEASE_JSON" | jq -r '.body // "QimingClaw 功能优化和问题修复。建议更新到最新版本以获得更好的体验。"' | head -c 500)
|
||||
PUB_DATE=$(echo "$RELEASE_JSON" | jq -r '.createdAt // empty')
|
||||
if [ -z "$PUB_DATE" ]; then
|
||||
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||||
fi
|
||||
|
||||
add_platform() {
|
||||
local key="$1" file="$2"
|
||||
local filepath="${ASSETS_DIR}/${file}"
|
||||
if [ -f "$filepath" ]; then
|
||||
local sig size
|
||||
sig=$(sha512sum "$filepath" | awk '{print $1}' | xxd -r -p | base64 -w0)
|
||||
if [ -z "$sig" ]; then
|
||||
echo " Skipped: ${key} -> ${file} (signature generation failed)"
|
||||
return 0
|
||||
fi
|
||||
size=$(stat -c%s "$filepath" 2>/dev/null || stat -f%z "$filepath")
|
||||
PLATFORMS=$(echo "$PLATFORMS" | jq \
|
||||
--arg k "$key" \
|
||||
--arg u "${OSS_URL_PREFIX}/${file}" \
|
||||
--arg s "$sig" \
|
||||
--argjson sz "$size" \
|
||||
'. + {($k): {"url": $u, "signature": $s, "size": $sz}}')
|
||||
echo " Added: ${key} -> ${file} (${size} bytes)"
|
||||
fi
|
||||
}
|
||||
|
||||
PLATFORMS="{}"
|
||||
echo "==> Scanning release assets for platforms..."
|
||||
add_platform "darwin-aarch64" "QimingClaw-${VERSION}-arm64.dmg"
|
||||
add_platform "darwin-aarch64-zip" "QimingClaw-${VERSION}-arm64-mac.zip"
|
||||
add_platform "darwin-x86_64" "QimingClaw-${VERSION}.dmg"
|
||||
add_platform "darwin-x86_64-zip" "QimingClaw-${VERSION}-mac.zip"
|
||||
add_platform "windows-x86_64" "QimingClaw.Setup.${VERSION}.exe"
|
||||
add_platform "windows-x86_64-nsis" "QimingClaw.Setup.${VERSION}.exe"
|
||||
add_platform "windows-x86_64-msi" "QimingClaw.${VERSION}.msi"
|
||||
add_platform "linux-x86_64" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-appimage" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-deb" "QimingClaw-${VERSION}-amd64.deb"
|
||||
add_platform "linux-x86_64-rpm" "QimingClaw-${VERSION}-x86_64.rpm"
|
||||
add_platform "linux-aarch64" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-appimage" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-deb" "QimingClaw-${VERSION}-arm64.deb"
|
||||
add_platform "linux-aarch64-rpm" "QimingClaw-${VERSION}-aarch64.rpm"
|
||||
|
||||
if [ "$(echo "$PLATFORMS" | jq 'length')" = "0" ]; then
|
||||
echo "::warning::No exact filename matches, falling back to fuzzy scan"
|
||||
for file in "${ASSETS_DIR}"/*; do
|
||||
filename=$(basename "$file")
|
||||
case "$filename" in
|
||||
*.dmg|*.zip|*.exe|*.msi|*.AppImage|*.deb|*.rpm)
|
||||
key=""
|
||||
case "$filename" in
|
||||
*arm64*mac*|*arm64*.dmg) key="darwin-aarch64" ;;
|
||||
*x64*mac*|*x64*.dmg) key="darwin-x86_64" ;;
|
||||
*Setup*.exe|*setup*.exe) key="windows-x86_64" ;;
|
||||
*.msi) key="windows-x86_64-msi" ;;
|
||||
*arm64*.AppImage) key="linux-aarch64" ;;
|
||||
*amd64*.AppImage|*x64*.AppImage|*.AppImage) key="linux-x86_64" ;;
|
||||
*arm64*.deb) key="linux-aarch64-deb" ;;
|
||||
*amd64*.deb|*x64*.deb) key="linux-x86_64-deb" ;;
|
||||
*aarch64*.rpm) key="linux-aarch64-rpm" ;;
|
||||
*x86_64*.rpm|*x64*.rpm) key="linux-x86_64-rpm" ;;
|
||||
esac
|
||||
[ -n "$key" ] && add_platform "$key" "$filename"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
LATEST_JSON=$(jq -n \
|
||||
--arg version "$VERSION" \
|
||||
--arg notes "$NOTES" \
|
||||
--arg pub_date "$PUB_DATE" \
|
||||
--argjson platforms "$PLATFORMS" \
|
||||
'{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms}')
|
||||
echo "$LATEST_JSON" > /tmp/latest.json
|
||||
echo "==> Generated latest.json"
|
||||
echo "$LATEST_JSON" | jq .
|
||||
|
||||
# 从 latest.json 的 platforms 生成 electron-updater 所需的 yml(url 使用相对路径,feed 基地址为 OSS 版本化路径)
|
||||
# 格式: version, files: [{ url, sha512, size }], path, sha512, releaseDate
|
||||
gen_yml() {
|
||||
local channel="$1"
|
||||
local keys="$2"
|
||||
local first_key first_file
|
||||
first_key=$(echo "$PLATFORMS" | jq -r --argjson keys "$keys" 'limit(1; $keys[] as $k | select(.[$k] != null) | $k)')
|
||||
[ -z "$first_key" ] || [ "$first_key" = "null" ] && return 0
|
||||
first_file=$(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].url' | sed 's|.*/||')
|
||||
{
|
||||
echo "version: $VERSION"
|
||||
echo "path: $first_file"
|
||||
echo "sha512: $(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].signature')"
|
||||
echo "releaseDate: $PUB_DATE"
|
||||
echo "files:"
|
||||
echo "$PLATFORMS" | jq -r --argjson keys "$keys" '
|
||||
[ $keys[] as $k | select(.[$k] != null) | .[$k] |
|
||||
" - url: " + (.url | split("/") | last) + "\n sha512: " + .signature + "\n size: " + (.size | tostring)
|
||||
] | join("\n")
|
||||
'
|
||||
} > "${ASSETS_DIR}/${channel}.yml"
|
||||
echo " Generated ${channel}.yml (primary: $first_file)"
|
||||
}
|
||||
# 与 macOS 一致:所有 yml 均由同一份 latest.json 的 platforms 派生,Linux 也走 latest/latest.json 的更新链路
|
||||
DARWIN_KEYS='["darwin-aarch64-zip","darwin-x86_64-zip","darwin-aarch64","darwin-x86_64"]'
|
||||
LINUX_KEYS='["linux-x86_64-appimage","linux-aarch64-appimage","linux-x86_64","linux-aarch64","linux-x86_64-deb","linux-aarch64-deb","linux-x86_64-rpm","linux-aarch64-rpm"]'
|
||||
LINUX_ARM64_KEYS='["linux-aarch64-appimage","linux-aarch64","linux-aarch64-deb","linux-aarch64-rpm"]'
|
||||
LINUX_X64_KEYS='["linux-x86_64-appimage","linux-x86_64","linux-x86_64-deb","linux-x86_64-rpm"]'
|
||||
WIN_KEYS='["windows-x86_64","windows-x86_64-nsis","windows-x86_64-msi"]'
|
||||
gen_yml "latest-mac" "$DARWIN_KEYS"
|
||||
gen_yml "latest-linux" "$LINUX_KEYS"
|
||||
# electron-updater 在 Linux 上按架构请求 latest-linux-arm64.yml / latest-linux-x64.yml,与 macOS 一样由 latest.json 派生
|
||||
gen_yml "latest-linux-arm64" "$LINUX_ARM64_KEYS"
|
||||
gen_yml "latest-linux-x64" "$LINUX_X64_KEYS"
|
||||
gen_yml "latest" "$WIN_KEYS"
|
||||
echo "==> Platform yml written to ${ASSETS_DIR}"
|
||||
|
||||
- name: Upload all assets to versioned path
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
OSS_PREFIX="qimingclaw-electron/${TAG}"
|
||||
|
||||
for file in /tmp/release-assets/*; do
|
||||
filename=$(basename "$file")
|
||||
echo "Uploading: ${filename}"
|
||||
ossutil cp --force "$file" "${OSS_BUCKET}/${OSS_PREFIX}/${filename}" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
done
|
||||
|
||||
# latest.json 已在上一段生成并写入 /tmp/latest.json,此处仅上传
|
||||
- name: Upload latest.json to OSS
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
echo "==> Publishing latest.json for channel: ${CHANNEL}"
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/${TAG}/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/latest/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
echo "==> Uploaded latest.json to stable pointer: latest/latest.json"
|
||||
else
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/beta/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
echo "==> Uploaded latest.json to beta pointer: beta/latest.json (stable pointer untouched)"
|
||||
fi
|
||||
|
||||
- name: Verify OSS upload
|
||||
run: |
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
VERIFY_URL="${OSS_CDN_BASE}/qimingclaw-electron/latest/latest.json"
|
||||
else
|
||||
VERIFY_URL="${OSS_CDN_BASE}/qimingclaw-electron/beta/latest.json"
|
||||
fi
|
||||
HTTP_STATUS=$(curl -sS -o /tmp/oss-latest.json -w "%{http_code}" "$VERIFY_URL")
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "==> latest.json (${CHANNEL}): OK (HTTP ${HTTP_STATUS})"
|
||||
cat /tmp/oss-latest.json | jq .
|
||||
else
|
||||
echo "::error::latest.json for channel '${CHANNEL}' not found on OSS (HTTP ${HTTP_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
334
qimingclaw/.github/workflows/sync-electron-to-oss.yml
vendored
Normal file
334
qimingclaw/.github/workflows/sync-electron-to-oss.yml
vendored
Normal file
@@ -0,0 +1,334 @@
|
||||
# 仅同步:将指定 tag 的 GitHub Release 资产同步到 MinIO S3(构建产物)和阿里云 OSS(latest.json)
|
||||
# 供手动 workflow_dispatch 触发。
|
||||
#
|
||||
# 所需 Secrets:GH_PAT(或 repo token)、MINIO_ACCESS_KEY_ID、MINIO_SECRET_ACCESS_KEY、OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET
|
||||
|
||||
name: Sync Electron Release to MinIO S3 & OSS
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag to sync (e.g. electron-v0.9.0)"
|
||||
required: true
|
||||
type: string
|
||||
channel:
|
||||
description: "Update channel pointer to publish (stable|beta). beta will NOT overwrite latest/latest.json"
|
||||
required: false
|
||||
default: stable
|
||||
type: choice
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-to-minio:
|
||||
name: Sync release to MinIO S3 & OSS
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
env:
|
||||
# MinIO S3 配置(存放构建产物和 yml 文件)
|
||||
S3_ENDPOINT: "https://s3.qiming.com:9443"
|
||||
S3_REGION: "us-east-1"
|
||||
S3_BUCKET: "qimingclaw"
|
||||
S3_CDN_BASE: "https://s3.qiming.com:9443/qimingclaw"
|
||||
# 阿里云 OSS 配置(仅存放 latest.json)
|
||||
OSS_ENDPOINT: "https://oss-rg-china-mainland.aliyuncs.com"
|
||||
OSS_REGION: "rg-china-mainland"
|
||||
OSS_BUCKET: "oss://qiming-packages"
|
||||
OSS_CDN_BASE: "https://qiming-packages.oss-rg-china-mainland.aliyuncs.com"
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_CHANNEL: ${{ inputs.channel || 'stable' }}
|
||||
steps:
|
||||
- name: Ensure awscli v2 (for S3)
|
||||
run: |
|
||||
if aws --version 2>/dev/null; then
|
||||
echo "AWS CLI already installed"
|
||||
else
|
||||
curl -fSL -o /tmp/awscliv2.zip \
|
||||
"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
|
||||
unzip -q /tmp/awscliv2.zip -d /tmp
|
||||
sudo /tmp/aws/install --bin-dir /usr/local/bin
|
||||
fi
|
||||
aws --version
|
||||
|
||||
- name: Install ossutil v2 (for latest.json)
|
||||
run: |
|
||||
curl -fSL -o /tmp/ossutil.zip \
|
||||
"https://gosspublic.alicdn.com/ossutil/v2/2.2.0/ossutil-2.2.0-linux-amd64.zip"
|
||||
unzip -q /tmp/ossutil.zip -d /tmp
|
||||
sudo mv /tmp/ossutil-2.2.0-linux-amd64/ossutil /usr/local/bin/
|
||||
ossutil version
|
||||
|
||||
- name: Configure AWS credentials for S3
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"
|
||||
aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY"
|
||||
aws configure set default.region "$S3_REGION"
|
||||
aws configure set default.s3.addressing_style path
|
||||
aws configure set endpoint_url "$S3_ENDPOINT"
|
||||
|
||||
- name: Download all release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
mkdir -p /tmp/release-assets
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir /tmp/release-assets
|
||||
echo "==> Downloaded assets:"
|
||||
ls -lh /tmp/release-assets/
|
||||
|
||||
- name: Generate latest.json and platform yml from assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
VERSION="${TAG#electron-v}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
S3_URL_PREFIX="${S3_CDN_BASE}/qimingclaw-electron/${TAG}"
|
||||
if [ "$CHANNEL" = "beta" ]; then
|
||||
S3_URL_PREFIX="${S3_CDN_BASE}/qimingclaw-electron/beta-build/${TAG}"
|
||||
fi
|
||||
ASSETS_DIR="/tmp/release-assets"
|
||||
|
||||
# semver MVP guard: only allow numeric x.y.z
|
||||
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::Invalid version '${VERSION}'. MVP only supports numeric x.y.z"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "beta" ]; then
|
||||
echo "::error::Invalid channel '${CHANNEL}', expected stable or beta"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "${ASSETS_DIR}"/*.yml
|
||||
|
||||
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body,createdAt 2>/dev/null || echo '{}')
|
||||
NOTES=$(echo "$RELEASE_JSON" | jq -r '.body // "QimingClaw 功能优化和问题修复。建议更新到最新版本以获得更好的体验。"' | head -c 500)
|
||||
PUB_DATE=$(echo "$RELEASE_JSON" | jq -r '.createdAt // empty')
|
||||
if [ -z "$PUB_DATE" ]; then
|
||||
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||||
fi
|
||||
|
||||
add_platform() {
|
||||
local key="$1" file="$2"
|
||||
local filepath="${ASSETS_DIR}/${file}"
|
||||
if [ -f "$filepath" ]; then
|
||||
local sig size
|
||||
sig=$(sha512sum "$filepath" | awk '{print $1}' | xxd -r -p | base64 -w0)
|
||||
if [ -z "$sig" ]; then
|
||||
echo " Skipped: ${key} -> ${file} (signature generation failed)"
|
||||
return 0
|
||||
fi
|
||||
size=$(stat -c%s "$filepath" 2>/dev/null || stat -f%z "$filepath")
|
||||
PLATFORMS=$(echo "$PLATFORMS" | jq \
|
||||
--arg k "$key" \
|
||||
--arg u "${S3_URL_PREFIX}/${file}" \
|
||||
--arg s "$sig" \
|
||||
--argjson sz "$size" \
|
||||
'. + {($k): {"url": $u, "signature": $s, "size": $sz}}')
|
||||
echo " Added: ${key} -> ${file} (${size} bytes)"
|
||||
fi
|
||||
}
|
||||
|
||||
PLATFORMS="{}"
|
||||
echo "==> Scanning release assets for platforms..."
|
||||
add_platform "darwin-aarch64" "QimingClaw-${VERSION}-arm64.dmg"
|
||||
add_platform "darwin-aarch64-zip" "QimingClaw-${VERSION}-arm64-mac.zip"
|
||||
add_platform "darwin-x86_64" "QimingClaw-${VERSION}.dmg"
|
||||
add_platform "darwin-x86_64-zip" "QimingClaw-${VERSION}-mac.zip"
|
||||
add_platform "windows-x86_64" "QimingClaw.Setup.${VERSION}.exe"
|
||||
add_platform "windows-x86_64-nsis" "QimingClaw.Setup.${VERSION}.exe"
|
||||
add_platform "windows-x86_64-msi" "QimingClaw.${VERSION}.msi"
|
||||
add_platform "linux-x86_64" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-appimage" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-deb" "QimingClaw-${VERSION}-amd64.deb"
|
||||
add_platform "linux-x86_64-rpm" "QimingClaw-${VERSION}-x86_64.rpm"
|
||||
add_platform "linux-aarch64" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-appimage" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-deb" "QimingClaw-${VERSION}-arm64.deb"
|
||||
add_platform "linux-aarch64-rpm" "QimingClaw-${VERSION}-aarch64.rpm"
|
||||
|
||||
if [ "$(echo "$PLATFORMS" | jq 'length')" = "0" ]; then
|
||||
echo "::warning::No exact filename matches, falling back to fuzzy scan"
|
||||
for file in "${ASSETS_DIR}"/*; do
|
||||
filename=$(basename "$file")
|
||||
case "$filename" in
|
||||
*.dmg|*.zip|*.exe|*.msi|*.AppImage|*.deb|*.rpm)
|
||||
key=""
|
||||
case "$filename" in
|
||||
*arm64*mac*|*arm64*.dmg) key="darwin-aarch64" ;;
|
||||
*x64*mac*|*x64*.dmg) key="darwin-x86_64" ;;
|
||||
*Setup*.exe|*setup*.exe) key="windows-x86_64" ;;
|
||||
*.msi) key="windows-x86_64-msi" ;;
|
||||
*arm64*.AppImage) key="linux-aarch64" ;;
|
||||
*amd64*.AppImage|*x64*.AppImage|*.AppImage) key="linux-x86_64" ;;
|
||||
*arm64*.deb) key="linux-aarch64-deb" ;;
|
||||
*amd64*.deb|*x64*.deb) key="linux-x86_64-deb" ;;
|
||||
*aarch64*.rpm) key="linux-aarch64-rpm" ;;
|
||||
*x86_64*.rpm|*x64*.rpm) key="linux-x86_64-rpm" ;;
|
||||
esac
|
||||
[ -n "$key" ] && add_platform "$key" "$filename"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
gen_yml() {
|
||||
local channel="$1"
|
||||
local keys="$2"
|
||||
local first_key first_file
|
||||
first_key=$(echo "$PLATFORMS" | jq -r --argjson keys "$keys" 'limit(1; $keys[] as $k | select(.[$k] != null) | $k)')
|
||||
[ -z "$first_key" ] || [ "$first_key" = "null" ] && return 0
|
||||
first_file=$(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].url' | sed 's|.*/||')
|
||||
{
|
||||
echo "version: $VERSION"
|
||||
echo "path: $first_file"
|
||||
echo "sha512: $(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].signature')"
|
||||
echo "releaseDate: $PUB_DATE"
|
||||
echo "files:"
|
||||
echo "$PLATFORMS" | jq -r --argjson keys "$keys" '
|
||||
[ $keys[] as $k | select(.[$k] != null) | .[$k] |
|
||||
" - url: " + (.url | split("/") | last) + "\n sha512: " + .signature + "\n size: " + (.size | tostring)
|
||||
] | join("\n")
|
||||
'
|
||||
} > "${ASSETS_DIR}/${channel}.yml"
|
||||
echo " Generated ${channel}.yml (primary: $first_file)"
|
||||
}
|
||||
DARWIN_KEYS='["darwin-aarch64-zip","darwin-x86_64-zip","darwin-aarch64","darwin-x86_64"]'
|
||||
LINUX_KEYS='["linux-x86_64-appimage","linux-aarch64-appimage","linux-x86_64","linux-aarch64","linux-x86_64-deb","linux-aarch64-deb","linux-x86_64-rpm","linux-aarch64-rpm"]'
|
||||
LINUX_ARM64_KEYS='["linux-aarch64-appimage","linux-aarch64","linux-aarch64-deb","linux-aarch64-rpm"]'
|
||||
LINUX_X64_KEYS='["linux-x86_64-appimage","linux-x86_64","linux-x86_64-deb","linux-x86_64-rpm"]'
|
||||
WIN_KEYS='["windows-x86_64","windows-x86_64-nsis","windows-x86_64-msi"]'
|
||||
gen_yml "latest-mac" "$DARWIN_KEYS"
|
||||
gen_yml "latest-linux" "$LINUX_KEYS"
|
||||
gen_yml "latest-linux-arm64" "$LINUX_ARM64_KEYS"
|
||||
gen_yml "latest-linux-x64" "$LINUX_X64_KEYS"
|
||||
gen_yml "latest" "$WIN_KEYS"
|
||||
echo "==> Platform yml written to ${ASSETS_DIR}"
|
||||
|
||||
# 构建 yml 文件的完整 S3 URL
|
||||
YML_URLS=$(jq -n \
|
||||
--arg darwin "${S3_URL_PREFIX}/latest-mac.yml" \
|
||||
--arg linux "${S3_URL_PREFIX}/latest-linux.yml" \
|
||||
--arg win "${S3_URL_PREFIX}/latest.yml" \
|
||||
'{darwin: $darwin, linux: $linux, win: $win}')
|
||||
|
||||
LATEST_JSON=$(jq -n \
|
||||
--arg version "$VERSION" \
|
||||
--arg notes "$NOTES" \
|
||||
--arg pub_date "$PUB_DATE" \
|
||||
--argjson platforms "$PLATFORMS" \
|
||||
--argjson yml "$YML_URLS" \
|
||||
'{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms, yml: $yml}')
|
||||
echo "$LATEST_JSON" > /tmp/latest.json
|
||||
echo "==> Generated latest.json"
|
||||
echo "$LATEST_JSON" | jq .
|
||||
|
||||
- name: Upload all assets to MinIO S3
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
if [ "$CHANNEL" = "beta" ]; then
|
||||
S3_PREFIX="qimingclaw-electron/beta-build/${TAG}"
|
||||
else
|
||||
S3_PREFIX="qimingclaw-electron/${TAG}"
|
||||
fi
|
||||
|
||||
for file in /tmp/release-assets/*; do
|
||||
filename=$(basename "$file")
|
||||
echo "Uploading to S3: ${filename}"
|
||||
aws s3 cp "$file" "s3://${S3_BUCKET}/${S3_PREFIX}/${filename}" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
done
|
||||
|
||||
# 同步 latest.json 到 S3(版本化路径 + 通道滚动指针)
|
||||
echo "Uploading latest.json to S3..."
|
||||
aws s3 cp /tmp/latest.json "s3://${S3_BUCKET}/${S3_PREFIX}/latest.json" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
aws s3 cp /tmp/latest.json "s3://${S3_BUCKET}/qimingclaw-electron/latest/latest.json" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
else
|
||||
aws s3 cp /tmp/latest.json "s3://${S3_BUCKET}/qimingclaw-electron/beta/latest.json" \
|
||||
--endpoint-url "$S3_ENDPOINT"
|
||||
fi
|
||||
|
||||
- name: Verify S3 upload
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
if [ "$CHANNEL" = "beta" ]; then
|
||||
VERIFY_URL="${S3_CDN_BASE}/qimingclaw-electron/beta-build/${TAG}/latest-mac.yml"
|
||||
else
|
||||
VERIFY_URL="${S3_CDN_BASE}/qimingclaw-electron/${TAG}/latest-mac.yml"
|
||||
fi
|
||||
HTTP_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" "$VERIFY_URL")
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "==> S3 upload verified: latest-mac.yml (HTTP ${HTTP_STATUS})"
|
||||
else
|
||||
echo "::error::S3 upload verification failed: latest-mac.yml (HTTP ${HTTP_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload latest.json to Alibaba OSS
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
echo "==> Publishing latest.json to OSS for channel: ${CHANNEL}"
|
||||
if [ "$CHANNEL" = "beta" ]; then
|
||||
VERSIONED_JSON_PATH="${OSS_BUCKET}/qimingclaw-electron/beta-build/${TAG}/latest.json"
|
||||
else
|
||||
VERSIONED_JSON_PATH="${OSS_BUCKET}/qimingclaw-electron/${TAG}/latest.json"
|
||||
fi
|
||||
ossutil cp --force /tmp/latest.json "${VERSIONED_JSON_PATH}" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/latest/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
echo "==> Uploaded latest.json to stable pointer: latest/latest.json"
|
||||
else
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/beta/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
echo "==> Uploaded latest.json to beta pointer: beta/latest.json (stable pointer untouched)"
|
||||
fi
|
||||
|
||||
- name: Verify OSS upload
|
||||
run: |
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
VERIFY_URL="${OSS_CDN_BASE}/qimingclaw-electron/latest/latest.json"
|
||||
else
|
||||
VERIFY_URL="${OSS_CDN_BASE}/qimingclaw-electron/beta/latest.json"
|
||||
fi
|
||||
HTTP_STATUS=$(curl -sS -o /tmp/oss-latest.json -w "%{http_code}" "$VERIFY_URL")
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "==> latest.json (${CHANNEL}): OK (HTTP ${HTTP_STATUS})"
|
||||
cat /tmp/oss-latest.json | jq .
|
||||
else
|
||||
echo "::error::latest.json for channel '${CHANNEL}' not found on OSS (HTTP ${HTTP_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
282
qimingclaw/.github/workflows/sync-electron-to-oss.yml.bak
vendored
Normal file
282
qimingclaw/.github/workflows/sync-electron-to-oss.yml.bak
vendored
Normal file
@@ -0,0 +1,282 @@
|
||||
# 仅同步:将指定 tag 的 GitHub Release 资产同步到阿里云 OSS(不构建)
|
||||
# 供 sync-oss.sh 通过 workflow_dispatch 触发;qimingclaw 仓库需复制本文件到 .github/workflows/ 后即可用。
|
||||
#
|
||||
# 所需 Secrets:GH_PAT(或 repo token)、OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET
|
||||
|
||||
name: Sync Electron Release to OSS
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag to sync (e.g. electron-v0.9.0)"
|
||||
required: true
|
||||
type: string
|
||||
channel:
|
||||
description: "Update channel pointer to publish (stable|beta). beta will NOT overwrite latest/latest.json"
|
||||
required: false
|
||||
default: stable
|
||||
type: choice
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-to-oss:
|
||||
name: Sync release assets to Alibaba Cloud OSS
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
env:
|
||||
OSS_ENDPOINT: "https://oss-rg-china-mainland.aliyuncs.com"
|
||||
OSS_REGION: "rg-china-mainland"
|
||||
OSS_BUCKET: "oss://qiming-packages"
|
||||
OSS_CDN_BASE: "https://qiming-packages.oss-rg-china-mainland.aliyuncs.com"
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_CHANNEL: ${{ inputs.channel || 'stable' }}
|
||||
steps:
|
||||
- name: Install ossutil v2
|
||||
run: |
|
||||
curl -fSL -o /tmp/ossutil.zip \
|
||||
"https://gosspublic.alicdn.com/ossutil/v2/2.2.0/ossutil-2.2.0-linux-amd64.zip"
|
||||
unzip -q /tmp/ossutil.zip -d /tmp
|
||||
sudo mv /tmp/ossutil-2.2.0-linux-amd64/ossutil /usr/local/bin/
|
||||
ossutil version
|
||||
|
||||
- name: Download all release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
mkdir -p /tmp/release-assets
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir /tmp/release-assets
|
||||
echo "==> Downloaded assets:"
|
||||
ls -lh /tmp/release-assets/
|
||||
|
||||
- name: Generate latest.json and platform yml from assets
|
||||
env:
|
||||
OSS_CDN_BASE: "https://qiming-packages.oss-rg-china-mainland.aliyuncs.com"
|
||||
GH_TOKEN: ${{ secrets.GH_PAT || github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
VERSION="${TAG#electron-v}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
OSS_URL_PREFIX="${OSS_CDN_BASE}/qimingclaw-electron/${TAG}"
|
||||
if [ "$CHANNEL" = "beta" ]; then
|
||||
OSS_URL_PREFIX="${OSS_CDN_BASE}/qimingclaw-electron/beta-build/${TAG}"
|
||||
fi
|
||||
ASSETS_DIR="/tmp/release-assets"
|
||||
|
||||
# semver MVP guard: only allow numeric x.y.z(用 grep -E,不依赖 ripgrep,与 ubuntu-latest 默认环境一致)
|
||||
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::Invalid version '${VERSION}'. MVP only supports numeric x.y.z"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "beta" ]; then
|
||||
echo "::error::Invalid channel '${CHANNEL}', expected stable or beta"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "${ASSETS_DIR}"/*.yml
|
||||
|
||||
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body,createdAt 2>/dev/null || echo '{}')
|
||||
NOTES=$(echo "$RELEASE_JSON" | jq -r '.body // "QimingClaw 功能优化和问题修复。建议更新到最新版本以获得更好的体验。"' | head -c 500)
|
||||
PUB_DATE=$(echo "$RELEASE_JSON" | jq -r '.createdAt // empty')
|
||||
if [ -z "$PUB_DATE" ]; then
|
||||
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||||
fi
|
||||
|
||||
add_platform() {
|
||||
local key="$1" file="$2"
|
||||
local filepath="${ASSETS_DIR}/${file}"
|
||||
if [ -f "$filepath" ]; then
|
||||
local sig size
|
||||
sig=$(sha512sum "$filepath" | awk '{print $1}' | xxd -r -p | base64 -w0)
|
||||
if [ -z "$sig" ]; then
|
||||
echo " Skipped: ${key} -> ${file} (signature generation failed)"
|
||||
return 0
|
||||
fi
|
||||
size=$(stat -c%s "$filepath" 2>/dev/null || stat -f%z "$filepath")
|
||||
PLATFORMS=$(echo "$PLATFORMS" | jq \
|
||||
--arg k "$key" \
|
||||
--arg u "${OSS_URL_PREFIX}/${file}" \
|
||||
--arg s "$sig" \
|
||||
--argjson sz "$size" \
|
||||
'. + {($k): {"url": $u, "signature": $s, "size": $sz}}')
|
||||
echo " Added: ${key} -> ${file} (${size} bytes)"
|
||||
fi
|
||||
}
|
||||
|
||||
PLATFORMS="{}"
|
||||
echo "==> Scanning release assets for platforms..."
|
||||
add_platform "darwin-aarch64" "QimingClaw-${VERSION}-arm64.dmg"
|
||||
add_platform "darwin-aarch64-zip" "QimingClaw-${VERSION}-arm64-mac.zip"
|
||||
add_platform "darwin-x86_64" "QimingClaw-${VERSION}.dmg"
|
||||
add_platform "darwin-x86_64-zip" "QimingClaw-${VERSION}-mac.zip"
|
||||
add_platform "windows-x86_64" "QimingClaw.Setup.${VERSION}.exe"
|
||||
add_platform "windows-x86_64-nsis" "QimingClaw.Setup.${VERSION}.exe"
|
||||
add_platform "windows-x86_64-msi" "QimingClaw.${VERSION}.msi"
|
||||
add_platform "linux-x86_64" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-appimage" "QimingClaw-${VERSION}.AppImage"
|
||||
add_platform "linux-x86_64-deb" "QimingClaw-${VERSION}-amd64.deb"
|
||||
add_platform "linux-x86_64-rpm" "QimingClaw-${VERSION}-x86_64.rpm"
|
||||
add_platform "linux-aarch64" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-appimage" "QimingClaw-${VERSION}-arm64.AppImage"
|
||||
add_platform "linux-aarch64-deb" "QimingClaw-${VERSION}-arm64.deb"
|
||||
add_platform "linux-aarch64-rpm" "QimingClaw-${VERSION}-aarch64.rpm"
|
||||
|
||||
if [ "$(echo "$PLATFORMS" | jq 'length')" = "0" ]; then
|
||||
echo "::warning::No exact filename matches, falling back to fuzzy scan"
|
||||
for file in "${ASSETS_DIR}"/*; do
|
||||
filename=$(basename "$file")
|
||||
case "$filename" in
|
||||
*.dmg|*.zip|*.exe|*.msi|*.AppImage|*.deb|*.rpm)
|
||||
key=""
|
||||
case "$filename" in
|
||||
*arm64*mac*|*arm64*.dmg) key="darwin-aarch64" ;;
|
||||
*x64*mac*|*x64*.dmg) key="darwin-x86_64" ;;
|
||||
*Setup*.exe|*setup*.exe) key="windows-x86_64" ;;
|
||||
*.msi) key="windows-x86_64-msi" ;;
|
||||
*arm64*.AppImage) key="linux-aarch64" ;;
|
||||
*amd64*.AppImage|*x64*.AppImage|*.AppImage) key="linux-x86_64" ;;
|
||||
*arm64*.deb) key="linux-aarch64-deb" ;;
|
||||
*amd64*.deb|*x64*.deb) key="linux-x86_64-deb" ;;
|
||||
*aarch64*.rpm) key="linux-aarch64-rpm" ;;
|
||||
*x86_64*.rpm|*x64*.rpm) key="linux-x86_64-rpm" ;;
|
||||
esac
|
||||
[ -n "$key" ] && add_platform "$key" "$filename"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
gen_yml() {
|
||||
local channel="$1"
|
||||
local keys="$2"
|
||||
local first_key first_file
|
||||
first_key=$(echo "$PLATFORMS" | jq -r --argjson keys "$keys" 'limit(1; $keys[] as $k | select(.[$k] != null) | $k)')
|
||||
[ -z "$first_key" ] || [ "$first_key" = "null" ] && return 0
|
||||
first_file=$(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].url' | sed 's|.*/||')
|
||||
{
|
||||
echo "version: $VERSION"
|
||||
echo "path: $first_file"
|
||||
echo "sha512: $(echo "$PLATFORMS" | jq -r --arg k "$first_key" '.[$k].signature')"
|
||||
echo "releaseDate: $PUB_DATE"
|
||||
echo "files:"
|
||||
echo "$PLATFORMS" | jq -r --argjson keys "$keys" '
|
||||
[ $keys[] as $k | select(.[$k] != null) | .[$k] |
|
||||
" - url: " + (.url | split("/") | last) + "\n sha512: " + .signature + "\n size: " + (.size | tostring)
|
||||
] | join("\n")
|
||||
'
|
||||
} > "${ASSETS_DIR}/${channel}.yml"
|
||||
echo " Generated ${channel}.yml (primary: $first_file)"
|
||||
}
|
||||
DARWIN_KEYS='["darwin-aarch64-zip","darwin-x86_64-zip","darwin-aarch64","darwin-x86_64"]'
|
||||
LINUX_KEYS='["linux-x86_64-appimage","linux-aarch64-appimage","linux-x86_64","linux-aarch64","linux-x86_64-deb","linux-aarch64-deb","linux-x86_64-rpm","linux-aarch64-rpm"]'
|
||||
LINUX_ARM64_KEYS='["linux-aarch64-appimage","linux-aarch64","linux-aarch64-deb","linux-aarch64-rpm"]'
|
||||
LINUX_X64_KEYS='["linux-x86_64-appimage","linux-x86_64","linux-x86_64-deb","linux-x86_64-rpm"]'
|
||||
WIN_KEYS='["windows-x86_64","windows-x86_64-nsis","windows-x86_64-msi"]'
|
||||
gen_yml "latest-mac" "$DARWIN_KEYS"
|
||||
gen_yml "latest-linux" "$LINUX_KEYS"
|
||||
gen_yml "latest-linux-arm64" "$LINUX_ARM64_KEYS"
|
||||
gen_yml "latest-linux-x64" "$LINUX_X64_KEYS"
|
||||
gen_yml "latest" "$WIN_KEYS"
|
||||
echo "==> Platform yml written to ${ASSETS_DIR}"
|
||||
|
||||
# 构建 yml 文件的完整 OSS URL(供客户端 electron-updater 直接使用)
|
||||
# 注意:stable 通道 OSS_URL_PREFIX = qimingclaw-electron/electron-v{x}.y.z
|
||||
# beta 通道 OSS_URL_PREFIX = qimingclaw-electron/beta-build/prerelease-v{x}.y.z
|
||||
YML_URLS=$(jq -n \
|
||||
--arg darwin "${OSS_URL_PREFIX}/latest-mac.yml" \
|
||||
--arg linux "${OSS_URL_PREFIX}/latest-linux.yml" \
|
||||
--arg win "${OSS_URL_PREFIX}/latest.yml" \
|
||||
'{darwin: $darwin, linux: $linux, win: $win}')
|
||||
|
||||
LATEST_JSON=$(jq -n \
|
||||
--arg version "$VERSION" \
|
||||
--arg notes "$NOTES" \
|
||||
--arg pub_date "$PUB_DATE" \
|
||||
--argjson platforms "$PLATFORMS" \
|
||||
--argjson yml "$YML_URLS" \
|
||||
'{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms, yml: $yml}')
|
||||
echo "$LATEST_JSON" > /tmp/latest.json
|
||||
echo "==> Generated latest.json"
|
||||
echo "$LATEST_JSON" | jq .
|
||||
|
||||
- name: Upload all assets to versioned path
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
if [ "$CHANNEL" = "beta" ]; then
|
||||
OSS_PREFIX="qimingclaw-electron/beta-build/${TAG}"
|
||||
else
|
||||
OSS_PREFIX="qimingclaw-electron/${TAG}"
|
||||
fi
|
||||
for file in /tmp/release-assets/*; do
|
||||
filename=$(basename "$file")
|
||||
echo "Uploading: ${filename}"
|
||||
ossutil cp --force "$file" "${OSS_BUCKET}/${OSS_PREFIX}/${filename}" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
done
|
||||
|
||||
- name: Upload latest.json to OSS
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
|
||||
echo "==> Publishing latest.json for channel: ${CHANNEL}"
|
||||
if [ "$CHANNEL" = "beta" ]; then
|
||||
VERSIONED_JSON_PATH="${OSS_BUCKET}/qimingclaw-electron/beta-build/${TAG}/latest.json"
|
||||
else
|
||||
VERSIONED_JSON_PATH="${OSS_BUCKET}/qimingclaw-electron/${TAG}/latest.json"
|
||||
fi
|
||||
ossutil cp --force /tmp/latest.json "${VERSIONED_JSON_PATH}" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/latest/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
echo "==> Uploaded latest.json to stable pointer: latest/latest.json"
|
||||
else
|
||||
ossutil cp --force /tmp/latest.json "${OSS_BUCKET}/qimingclaw-electron/beta/latest.json" \
|
||||
--endpoint "$OSS_ENDPOINT" \
|
||||
--region "$OSS_REGION" \
|
||||
--access-key-id "$OSS_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$OSS_ACCESS_KEY_SECRET"
|
||||
echo "==> Uploaded latest.json to beta pointer: beta/latest.json (stable pointer untouched)"
|
||||
fi
|
||||
|
||||
- name: Verify OSS upload
|
||||
run: |
|
||||
CHANNEL="${RELEASE_CHANNEL}"
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
VERIFY_URL="${OSS_CDN_BASE}/qimingclaw-electron/latest/latest.json"
|
||||
else
|
||||
VERIFY_URL="${OSS_CDN_BASE}/qimingclaw-electron/beta/latest.json"
|
||||
fi
|
||||
HTTP_STATUS=$(curl -sS -o /tmp/oss-latest.json -w "%{http_code}" "$VERIFY_URL")
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "==> latest.json (${CHANNEL}): OK (HTTP ${HTTP_STATUS})"
|
||||
cat /tmp/oss-latest.json | jq .
|
||||
else
|
||||
echo "::error::latest.json for channel '${CHANNEL}' not found on OSS (HTTP ${HTTP_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user