# ClawPanel 发布构建工作流 # 推送 v* 标签时自动构建跨平台产物并创建 GitHub Release name: Release on: push: tags: - "v*" workflow_dispatch: inputs: tag_name: description: '发布版本号 (例如: v1.0.0)' required: true type: string default: 'v1.0.0' jobs: validate-release: name: 验证发布目标 runs-on: ubuntu-latest permissions: contents: read outputs: tag_name: ${{ steps.release.outputs.tag_name }} version: ${{ steps.release.outputs.version }} steps: - name: 签出代码和标签 uses: actions/checkout@v4 with: fetch-depth: 0 - name: 验证 SemVer、Tag 绑定与 Release 唯一性 id: release shell: bash env: GH_TOKEN: ${{ github.token }} INPUT_TAG: ${{ github.event.inputs.tag_name }} run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then TAG_NAME="$INPUT_TAG" else TAG_NAME="${{ github.ref_name }}" fi SEMVER_PATTERN='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' if [[ ! "$TAG_NAME" =~ $SEMVER_PATTERN ]]; then echo "无效发布标签: $TAG_NAME(必须是 vX.Y.Z 格式的 SemVer)" >&2 exit 1 fi TAG_COMMIT=$(git rev-parse "refs/tags/$TAG_NAME^{commit}" 2>/dev/null) || { echo "发布标签不存在: $TAG_NAME" >&2 exit 1 } HEAD_COMMIT=$(git rev-parse HEAD) if [ "$TAG_COMMIT" != "$HEAD_COMMIT" ]; then echo "发布标签 $TAG_NAME 未绑定当前 ref ($HEAD_COMMIT)" >&2 exit 1 fi if gh release view "$TAG_NAME" >/dev/null 2>&1; then echo "Release $TAG_NAME 已存在,禁止覆盖" >&2 exit 1 fi echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" echo "version=${TAG_NAME#v}" >> "$GITHUB_OUTPUT" test-release: name: 发布前单元测试 needs: validate-release runs-on: ubuntu-latest steps: - name: 签出代码 uses: actions/checkout@v4 - name: 安装 Node.js uses: actions/setup-node@v4 with: node-version: 22.22.3 cache: npm - name: 安装前端依赖 run: npm ci - name: Node 单元测试 run: node --test tests/*.test.js - name: 安装 Rust 工具链 uses: dtolnay/rust-toolchain@stable - name: Rust 编译缓存 uses: swatinem/rust-cache@v2 with: workspaces: src-tauri -> target - name: 安装 Linux 系统依赖 run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ librsvg2-dev \ patchelf \ libssl-dev \ libgtk-3-dev \ libayatana-appindicator3-dev - name: Rust 单元测试 working-directory: src-tauri run: cargo test --locked # ── 跨平台构建 job ───────────────────────────────────────────────────────── build: name: 构建 (${{ matrix.platform.name }}) needs: [validate-release, test-release] runs-on: ${{ matrix.platform.os }} permissions: contents: read strategy: fail-fast: false matrix: platform: - name: macOS (Apple Silicon) os: macos-latest args: --target aarch64-apple-darwin rust_target: aarch64-apple-darwin artifact: macos-arm64 - name: macOS (Intel) os: macos-latest args: --target x86_64-apple-darwin rust_target: x86_64-apple-darwin artifact: macos-x64 - name: Linux (x64) os: ubuntu-latest args: "" rust_target: "" artifact: linux-x64 - name: Windows (x64) os: windows-latest args: "" rust_target: "" artifact: windows-x64 - name: Windows (x64) 完整包 os: windows-latest args: "" rust_target: "" artifact: windows-x64-full steps: - name: 签出代码 uses: actions/checkout@v4 with: fetch-depth: 0 - name: 安装 Node.js uses: actions/setup-node@v4 with: node-version: 22.22.3 cache: npm - name: 安装前端依赖 run: npm ci - name: 同步版本号到构建产物 shell: bash run: | node scripts/sync-version.js "${{ needs.validate-release.outputs.version }}" - name: 安装 Rust 工具链 uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.platform.rust_target }} - name: Rust 编译缓存 uses: swatinem/rust-cache@v2 with: workspaces: src-tauri -> target key: ${{ matrix.platform.name }} save-if: false - name: 安装 Linux 系统依赖 if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ librsvg2-dev \ patchelf \ libssl-dev \ libgtk-3-dev \ libayatana-appindicator3-dev - name: 构建 Tauri 应用 if: matrix.platform.name != 'Windows (x64) 完整包' run: npx tauri build ${{ matrix.platform.args }} -- --locked - name: 上传平台构建产物 if: matrix.platform.name != 'Windows (x64) 完整包' uses: actions/upload-artifact@v4 with: name: release-${{ matrix.platform.artifact }} path: | src-tauri/target/**/release/bundle/**/*.dmg src-tauri/target/**/release/bundle/**/*.exe src-tauri/target/**/release/bundle/**/*.msi src-tauri/target/**/release/bundle/**/*.AppImage src-tauri/target/**/release/bundle/**/*.deb src-tauri/target/**/release/bundle/**/*.rpm if-no-files-found: error compression-level: 0 # ── Windows 完整包(内嵌 WebView2 离线安装器)────────────────────────────── - name: 配置 WebView2 完整包模式 if: matrix.platform.name == 'Windows (x64) 完整包' shell: bash run: | node -e " const fs = require('fs'); const f = 'src-tauri/tauri.conf.json'; const c = JSON.parse(fs.readFileSync(f, 'utf8')); c.bundle.windows.webviewInstallMode = { type: 'offlineInstaller', silent: true }; fs.writeFileSync(f, JSON.stringify(c, null, 2)); " - name: 构建 Windows 完整包 if: matrix.platform.name == 'Windows (x64) 完整包' shell: bash run: npx tauri build -- --locked - name: 重命名 Windows 完整包 if: matrix.platform.name == 'Windows (x64) 完整包' shell: bash run: | VERSION="${{ needs.validate-release.outputs.version }}" BUNDLE_DIR="src-tauri/target/release/bundle" mv "${BUNDLE_DIR}/nsis/ClawPanel_${VERSION}_x64-setup.exe" \ "${BUNDLE_DIR}/nsis/ClawPanel_${VERSION}_x64-setup-full.exe" mv "${BUNDLE_DIR}/msi/ClawPanel_${VERSION}_x64_en-US.msi" \ "${BUNDLE_DIR}/msi/ClawPanel_${VERSION}_x64-full_en-US.msi" - name: 上传 Windows 完整包产物 if: matrix.platform.name == 'Windows (x64) 完整包' uses: actions/upload-artifact@v4 with: name: release-${{ matrix.platform.artifact }} path: | src-tauri/target/release/bundle/nsis/ClawPanel_${{ needs.validate-release.outputs.version }}_x64-setup-full.exe src-tauri/target/release/bundle/msi/ClawPanel_${{ needs.validate-release.outputs.version }}_x64-full_en-US.msi if-no-files-found: error compression-level: 0 build-web: name: 构建 Web 热更新包 needs: [validate-release, test-release] runs-on: ubuntu-latest steps: - name: 签出代码 uses: actions/checkout@v4 - name: 安装 Node.js uses: actions/setup-node@v4 with: node-version: 22.22.3 cache: npm - name: 安装前端依赖 run: npm ci - name: 构建前端 run: npm run build - name: 打包 Web 热更新产物 shell: bash run: | VERSION="${{ needs.validate-release.outputs.version }}" cd dist zip -r "../web-${VERSION}.zip" . cd .. - name: 上传 Web workflow artifact uses: actions/upload-artifact@v4 with: name: release-web path: web-${{ needs.validate-release.outputs.version }}.zip if-no-files-found: error compression-level: 0 # ── 所有构建成功后一次性创建并公开 Release ──────────────────────────────── publish-release: name: 发布 Release needs: [validate-release, test-release, build, build-web] runs-on: ubuntu-latest permissions: contents: write env: TAG_NAME: ${{ needs.validate-release.outputs.tag_name }} VERSION: ${{ needs.validate-release.outputs.version }} steps: - name: 签出发布代码 uses: actions/checkout@v4 with: fetch-depth: 0 - name: 下载全部 workflow artifacts uses: actions/download-artifact@v4 with: pattern: release-* path: release-assets merge-multiple: true - name: 生成 Release Notes shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_RESULT: ${{ needs.build.result }} REPO: ${{ github.repository }} run: | DL="https://github.com/${REPO}/releases/download/${TAG_NAME}" # ── 生成分类 Changelog ── PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v' | sed -n '2p' || echo "") if [ -n "$PREV_TAG" ]; then RANGE="${PREV_TAG}..HEAD" CHANGELOG_HEADER="自 ${PREV_TAG} 以来的变更" else RANGE="" CHANGELOG_HEADER="主要变更" fi # 优先使用人工整理的 CHANGELOG.md 当前版本段,避免 Release Notes 只显示 release commit。 CHANGELOG_BODY=$(python3 - "$VERSION" <<'PY' import pathlib import re import sys version = sys.argv[1] path = pathlib.Path("CHANGELOG.md") if not path.exists(): sys.exit(0) text = path.read_text(encoding="utf-8") current = re.search(rf"^## \[{re.escape(version)}\](?:\s+-\s+.+)?\s*$", text, re.MULTILINE) if not current: sys.exit(0) rest = text[current.end():] next_version = re.search(r"^## \[", rest, re.MULTILINE) section = rest[:next_version.start()] if next_version else rest print(section.strip()) PY ) if [ -z "$CHANGELOG_BODY" ]; then # CHANGELOG.md 缺失时,退回 conventional commit 前缀分类。 get_logs() { local prefix="$1" if [ -n "$RANGE" ]; then git log "$RANGE" --pretty=format:"%s" --no-merges | { grep -iE "^${prefix}" || true; } | sed "s/^${prefix}[:(] */- /" | head -20 else git log --pretty=format:"%s" --no-merges -30 | { grep -iE "^${prefix}" || true; } | sed "s/^${prefix}[:(] */- /" | head -20 fi } FEATS=$(get_logs "feat") FIXES=$(get_logs "fix") OTHERS=$( if [ -n "$RANGE" ]; then git log "$RANGE" --pretty=format:"%s" --no-merges | { grep -ivE "^(feat|fix|style|chore|ci|docs|test|build)" || true; } | sed 's/^/- /' | head -10 else git log --pretty=format:"%s" --no-merges -30 | { grep -ivE "^(feat|fix|style|chore|ci|docs|test|build)" || true; } | sed 's/^/- /' | head -10 fi ) if [ -n "$FEATS" ]; then CHANGELOG_BODY="${CHANGELOG_BODY}"$'\n### ✨ 新功能\n'"${FEATS}"$'\n' fi if [ -n "$FIXES" ]; then CHANGELOG_BODY="${CHANGELOG_BODY}"$'\n### 🐛 修复\n'"${FIXES}"$'\n' fi if [ -n "$OTHERS" ]; then CHANGELOG_BODY="${CHANGELOG_BODY}"$'\n### 📦 其他\n'"${OTHERS}"$'\n' fi fi if [ -z "$CHANGELOG_BODY" ]; then CHANGELOG_BODY=$'\n- 常规更新与优化\n' fi # ── 构建状态 ── if [ "$BUILD_RESULT" = "success" ]; then STATUS_BADGE="✅ 全部平台构建成功" else STATUS_BADGE="⚠️ 部分平台构建失败,请查看 [Actions 日志](https://github.com/${REPO}/actions)" fi # ── 写入 Release Notes ── { echo "${STATUS_BADGE}" echo "" echo "## 📥 下载安装" echo "" echo "根据你的操作系统选择对应安装包,点击文件名即可下载:" echo "" echo "### macOS" echo "| 芯片 | 安装包 |" echo "|------|--------|" echo "| Apple Silicon (M1/M2/M3/M4) | [ClawPanel_${VERSION}_aarch64.dmg](${DL}/ClawPanel_${VERSION}_aarch64.dmg) |" echo "| Intel | [ClawPanel_${VERSION}_x64.dmg](${DL}/ClawPanel_${VERSION}_x64.dmg) |" echo "" echo '> **⚠️ 首次打开提示"无法验证开发者"?** 在终端执行:`sudo xattr -rd com.apple.quarantine /Applications/ClawPanel.app`,或前往 **系统设置 → 隐私与安全性** 点击「仍要打开」。' echo "" echo "### Windows" echo "| 格式 | 安装包 | 说明 |" echo "|------|--------|------|" echo "| EXE 安装器(推荐) | [ClawPanel_${VERSION}_x64-setup.exe](${DL}/ClawPanel_${VERSION}_x64-setup.exe) | 轻量版 ~10 MB |" echo "| EXE 完整包 | [ClawPanel_${VERSION}_x64-setup-full.exe](${DL}/ClawPanel_${VERSION}_x64-setup-full.exe) | 含 WebView2 ~200 MB |" echo "| MSI 安装器 | [ClawPanel_${VERSION}_x64_en-US.msi](${DL}/ClawPanel_${VERSION}_x64_en-US.msi) | 轻量版 ~10 MB |" echo "| MSI 完整包 | [ClawPanel_${VERSION}_x64-full_en-US.msi](${DL}/ClawPanel_${VERSION}_x64-full_en-US.msi) | 含 WebView2 ~200 MB |" echo "" echo "> **💡 轻量版 vs 完整包**:Win10 1803+ 和 Win11 已预装 WebView2,推荐下载轻量版。如果是内网/断网环境,选择完整包。" echo "" echo "### Linux" echo "| 格式 | 安装包 |" echo "|------|--------|" echo "| AppImage(免安装) | [ClawPanel_${VERSION}_amd64.AppImage](${DL}/ClawPanel_${VERSION}_amd64.AppImage) |" echo "| DEB(Debian/Ubuntu) | [ClawPanel_${VERSION}_amd64.deb](${DL}/ClawPanel_${VERSION}_amd64.deb) |" echo "| RPM(Fedora/RHEL) | [ClawPanel-${VERSION}-1.x86_64.rpm](${DL}/ClawPanel-${VERSION}-1.x86_64.rpm) |" echo "" echo "---" echo "" echo "## 🚀 首次使用" echo "" echo "1. 安装并打开 ClawPanel" echo "2. 首次运行会自动检测 Node.js 环境和 OpenClaw CLI" echo "3. 如未安装 OpenClaw,按提示一键安装即可" echo "4. 安装完成后自动跳转仪表盘,开始使用" echo "" echo '> **系统要求**:ClawPanel Web 后端需要 Node.js 18+;运行 OpenClaw Gateway 会按当前 OpenClaw 的 engines.node 自动检测,OpenClaw 2026.7.1 要求 Node.js 22.22.3+(22.x)。' echo "" echo "---" echo "" echo "## ${CHANGELOG_HEADER}" echo "${CHANGELOG_BODY}" echo "---" echo "" echo "📖 [项目主页](https://github.com/${REPO}) · 💬 [反馈问题](https://github.com/${REPO}/issues) · 📣 [QQ 群](https://qt.cool/c/OpenClaw) · Telegram: https://t.me/clawpanel" } > release_body.md - name: 创建草稿 Release 并上传完整资产 shell: bash env: GH_TOKEN: ${{ github.token }} run: | mapfile -d '' ASSETS < <(find release-assets -type f -print0) if [ "${#ASSETS[@]}" -eq 0 ]; then echo "没有可发布的 workflow artifacts" >&2 exit 1 fi gh release create "$TAG_NAME" --title "ClawPanel $TAG_NAME" --notes-file release_body.md --draft gh release upload "$TAG_NAME" "${ASSETS[@]}" - name: 写入本次热更新清单 shell: bash run: | WEB_PACKAGE=$(find release-assets -type f -name "web-${VERSION}.zip" -print -quit) if [ -z "$WEB_PACKAGE" ]; then echo "缺少 web-${VERSION}.zip" >&2 exit 1 fi HASH=$(sha256sum "$WEB_PACKAGE" | cut -d' ' -f1) SIZE=$(stat -c%s "$WEB_PACKAGE") DL_URL="https://github.com/${{ github.repository }}/releases/download/${TAG_NAME}/web-${VERSION}.zip" git fetch origin main git checkout -B release-manifest origin/main cat > docs/update/latest.json << EOF { "version": "${VERSION}", "minAppVersion": "${VERSION}", "hash": "sha256:${HASH}", "url": "${DL_URL}", "size": ${SIZE}, "changelog": "", "releasedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" } EOF sed -i 's/^ //' docs/update/latest.json git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add docs/update/latest.json if ! git diff --cached --quiet; then git commit -m "ci: update latest.json for ${TAG_NAME}" git push origin HEAD:main fi - name: 一次性公开 Release shell: bash env: GH_TOKEN: ${{ github.token }} run: gh release edit "$TAG_NAME" --draft=false