# 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