fix(release): harden pre-release stability gates

This commit is contained in:
晴天
2026-07-12 12:50:38 -07:00
parent b36a85e230
commit f514761d59
30 changed files with 2536 additions and 380 deletions

View File

@@ -80,12 +80,21 @@ jobs:
# Rust 编译检查
- name: Rust 编译检查
working-directory: src-tauri
run: cargo check
run: cargo check --locked
# Rust Lint警告视为错误
- name: Rust Clippy
working-directory: src-tauri
run: cargo clippy --all-targets -- -D warnings
run: cargo clippy --locked --all-targets -- -D warnings
# Node 单元测试
- name: Node 单元测试
run: node --test tests/*.test.js
# Rust 单元测试
- name: Rust 单元测试
working-directory: src-tauri
run: cargo test --locked
# 前端构建验证
- name: 前端构建验证

View File

@@ -23,6 +23,7 @@ concurrency:
jobs:
build:
if: ${{ github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
steps:
- name: 签出代码

View File

@@ -15,12 +15,107 @@ on:
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.19.0
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: write
contents: read
strategy:
fail-fast: false
matrix:
@@ -29,22 +124,27 @@ jobs:
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: 签出代码
@@ -52,16 +152,6 @@ jobs:
with:
fetch-depth: 0
- name: 设置版本标签
id: vars
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> "$GITHUB_ENV"
else
echo "TAG_NAME=${{ github.ref_name }}" >> "$GITHUB_ENV"
fi
- name: 安装 Node.js
uses: actions/setup-node@v4
with:
@@ -74,8 +164,7 @@ jobs:
- name: 同步版本号到构建产物
shell: bash
run: |
VERSION="${TAG_NAME#v}"
node scripts/sync-version.js "$VERSION"
node scripts/sync-version.js "${{ needs.validate-release.outputs.version }}"
- name: 安装 Rust 工具链
uses: dtolnay/rust-toolchain@stable
@@ -103,26 +192,22 @@ jobs:
- name: 构建 Tauri 应用
if: matrix.platform.name != 'Windows (x64) 完整包'
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# macOS 代码签名(可选)
# APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
# APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
# APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
# APPLE_ID: ${{ secrets.APPLE_ID }}
# APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
# APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# Windows 代码签名(可选)
# TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: npx tauri build ${{ matrix.platform.args }} -- --locked
- name: 上传平台构建产物
if: matrix.platform.name != 'Windows (x64) 完整包'
uses: actions/upload-artifact@v4
with:
tagName: ${{ env.TAG_NAME }}
releaseName: "ClawPanel ${{ env.TAG_NAME }}"
releaseBody: "正在构建所有平台安装包,请稍候..."
releaseDraft: false
prerelease: false
args: ${{ matrix.platform.args }}
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 完整包模式
@@ -140,57 +225,37 @@ jobs:
- name: 构建 Windows 完整包
if: matrix.platform.name == 'Windows (x64) 完整包'
shell: bash
run: npx tauri build
run: npx tauri build -- --locked
- name: 重命名并上传完整包
- name: 重命名 Windows 完整包
if: matrix.platform.name == 'Windows (x64) 完整包'
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${TAG_NAME#v}"
VERSION="${{ needs.validate-release.outputs.version }}"
BUNDLE_DIR="src-tauri/target/release/bundle"
# 确保 Release 已存在(可能被其他 matrix job 的 tauri-action 创建)
gh release create "$TAG_NAME" --title "ClawPanel $TAG_NAME" \
--notes "正在构建所有平台安装包,请稍候..." 2>/dev/null || true
# 重命名 NSIS exe 并上传
mv "${BUNDLE_DIR}/nsis/ClawPanel_${VERSION}_x64-setup.exe" \
"${BUNDLE_DIR}/nsis/ClawPanel_${VERSION}_x64-setup-full.exe"
gh release upload "$TAG_NAME" \
"${BUNDLE_DIR}/nsis/ClawPanel_${VERSION}_x64-setup-full.exe" --clobber
# 重命名 MSI 并上传
mv "${BUNDLE_DIR}/msi/ClawPanel_${VERSION}_x64_en-US.msi" \
"${BUNDLE_DIR}/msi/ClawPanel_${VERSION}_x64-full_en-US.msi"
gh release upload "$TAG_NAME" \
"${BUNDLE_DIR}/msi/ClawPanel_${VERSION}_x64-full_en-US.msi" --clobber
# ── 所有平台构建完成后,统一更新 Release Notes ─────────────────────────────
# 独立 job 确保只执行一次,彻底避免多个 matrix job 的竞争条件
update-release-notes:
name: 更新 Release Notes
needs: build
- 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
if: always() && needs.build.result != 'cancelled'
permissions:
contents: write
steps:
- name: 签出代码
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 设置版本标签
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> "$GITHUB_ENV"
else
echo "TAG_NAME=${{ github.ref_name }}" >> "$GITHUB_ENV"
fi
- name: 安装 Node.js
uses: actions/setup-node@v4
@@ -198,63 +263,59 @@ jobs:
node-version: 22.19.0
cache: npm
- name: 构建前端并上传热更新包
- name: 安装前端依赖
run: npm ci
- name: 构建前端
run: npm run build
- name: 打包 Web 热更新产物
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${TAG_NAME#v}"
# 构建前端
npm ci
npm run build
# 打包 dist 目录为 zip
VERSION="${{ needs.validate-release.outputs.version }}"
cd dist
zip -r "../web-${VERSION}.zip" .
cd ..
# 计算 SHA-256
HASH=$(sha256sum "web-${VERSION}.zip" | cut -d' ' -f1)
SIZE=$(stat -c%s "web-${VERSION}.zip" 2>/dev/null || stat -f%z "web-${VERSION}.zip")
- 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 Asset
gh release upload "$TAG_NAME" "web-${VERSION}.zip" --clobber
# ── 所有构建成功后一次性创建并公开 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 }}
# 读取现有 minAppVersion前端热更新通常不需要更新 Rust 后端,保留旧值)
MIN_APP_VER=$(cat docs/update/latest.json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('minAppVersion','0.9.0'))" 2>/dev/null || echo "0.9.0")
steps:
- name: 签出发布代码
uses: actions/checkout@v4
with:
fetch-depth: 0
# 更新 docs/update/latest.json
DL_URL="https://github.com/${{ github.repository }}/releases/download/${TAG_NAME}/web-${VERSION}.zip"
cat > docs/update/latest.json << EOF
{
"version": "${VERSION}",
"minAppVersion": "${MIN_APP_VER}",
"hash": "sha256:${HASH}",
"url": "${DL_URL}",
"size": ${SIZE},
"changelog": "",
"releasedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
# 去掉 heredoc 缩进
sed -i 's/^ //' docs/update/latest.json
- name: 下载全部 workflow artifacts
uses: actions/download-artifact@v4
with:
pattern: release-*
path: release-assets
merge-multiple: true
# 提交 latest.json 到 main 分支
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add docs/update/latest.json
git commit -m "ci: update latest.json for ${TAG_NAME}" || true
git push origin HEAD:main || true
- name: 生成并更新 Release Notes
- name: 生成 Release Notes
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_RESULT: ${{ needs.build.result }}
REPO: ${{ github.repository }}
run: |
VERSION="${TAG_NAME#v}"
DL="https://github.com/${REPO}/releases/download/${TAG_NAME}"
# ── 生成分类 Changelog ──
@@ -385,4 +446,58 @@ jobs:
echo "📖 [项目主页](https://github.com/${REPO}) · 💬 [反馈问题](https://github.com/${REPO}/issues) · 📣 [QQ 群](https://qt.cool/c/OpenClaw) · Telegram: https://t.me/clawpanel"
} > release_body.md
gh release edit "$TAG_NAME" --notes-file 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