Skip to content

feat(skill): add upgrade command (0.2.9) #223

feat(skill): add upgrade command (0.2.9)

feat(skill): add upgrade command (0.2.9) #223

Workflow file for this run

# cli-box - CI门禁
# 在PR创建/更新及push到main时触发
#
# 门禁检查项 (Rust):
# 1. Rust 格式化检查 - cargo fmt
# 2. Rust Clippy - 代码规范 + 编译检查 (macOS, 需要系统框架)
# 3. Rust 单元测试 - cargo test (macOS)
# 4. 安全检查 - 硬编码密钥检测 + 依赖漏洞扫描
#
# @see CLAUDE.md 七、核心工作流程
name: CI Gate
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
permissions:
contents: read
pull-requests: write
checks: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
RUST_VERSION: '1.91'
jobs:
# ==================== Rust 格式化检查 ====================
rust-fmt:
name: Rust 格式化检查
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Rust ${{ env.RUST_VERSION }}
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
components: rustfmt
- name: 运行 cargo fmt
run: cargo fmt --all -- --check
# ==================== Rust Clippy (macOS) ====================
rust-clippy:
name: Rust Clippy
runs-on: macos-latest
timeout-minutes: 15
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Rust ${{ env.RUST_VERSION }}
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
components: clippy
- name: 设置 Rust 缓存
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
key: "v2-clippy"
- name: 运行 cargo clippy
run: cargo clippy -p cli-box-core -p cli-box-cli --all-targets -- -D warnings
# ==================== Rust 单元测试 (macOS) ====================
rust-test:
name: Rust 单元测试
runs-on: macos-latest
timeout-minutes: 15
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Xcode
run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
- name: 安装 Rust ${{ env.RUST_VERSION }}
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: 设置 Rust 缓存
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
key: "v2-test"
- name: 清理 Rust 缓存
run: cargo clean 2>/dev/null || true
- name: 运行 cargo test
run: cargo test -p cli-box-core
- name: 安装 cargo-llvm-cov
run: |
rustup component add llvm-tools-preview
cargo install cargo-llvm-cov --locked
- name: 运行测试覆盖率
run: |
mkdir -p coverage
cargo llvm-cov -p cli-box-core --cobertura --output-path coverage/cobertura.xml
- name: 生成覆盖率摘要
if: always()
run: |
SUMMARY_FILE="rust-coverage-summary.md"
echo "## Rust 测试覆盖率" > "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
if [ -f coverage/cobertura.xml ]; then
python3 -c "
import xml.etree.ElementTree as ET
tree = ET.parse('coverage/cobertura.xml')
root = tree.getroot()
rate = float(root.attrib.get('line-rate', 0)) * 100
branch_rate = float(root.attrib.get('branch-rate', 0)) * 100
print(f'| 指标 | 覆盖率 |')
print(f'|------|--------|')
print(f'| 行覆盖率 | {rate:.1f}% |')
print(f'| 分支覆盖率 | {branch_rate:.1f}% |')
print()
print('| 模块 | 行覆盖率 |')
print('|------|----------|')
for pkg in root.findall('.//package'):
name = pkg.attrib.get('name', 'unknown')
pkg_rate = float(pkg.attrib.get('line-rate', 0)) * 100
bar_len = int(pkg_rate / 5)
bar = '█' * bar_len + '░' * (20 - bar_len)
print(f'| {name} | {bar} {pkg_rate:.1f}% |')
" >> "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
echo "> 详细报告见 Rust 覆盖率 artifact" >> "$SUMMARY_FILE"
else
echo "> ⚠️ 未生成覆盖率报告" >> "$SUMMARY_FILE"
echo "❌ 覆盖率生成失败:cobertura.xml 未生成" >> "$SUMMARY_FILE"
echo "::error::覆盖率报告生成失败,检查 llvm-cov 输出的测试失败信息"
exit 1
fi
echo "" >> "$SUMMARY_FILE"
cat "$SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY"
- name: 上传覆盖率摘要
uses: actions/upload-artifact@v4
if: always()
with:
name: rust-coverage-summary
path: rust-coverage-summary.md
retention-days: 1
- name: 上传覆盖率报告
uses: actions/upload-artifact@v4
if: always()
with:
name: rust-coverage
path: coverage/
retention-days: 14
# ==================== 前端测试 + 类型检查 + 覆盖率 ====================
frontend-test:
name: 前端测试 & 覆盖率
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: 安装 pnpm
uses: pnpm/action-setup@v4
with:
version: "10"
- name: 安装依赖
working-directory: electron-app
run: pnpm install --frozen-lockfile
- name: TypeScript 类型检查
working-directory: electron-app
run: pnpm typecheck
- name: 运行前端测试 + 覆盖率
working-directory: electron-app
run: pnpm vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=text
- name: 生成前端覆盖率摘要
if: always()
working-directory: electron-app
run: |
SUMMARY_FILE="frontend-coverage-summary.md"
echo "## 前端测试覆盖率" > "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
if [ -f coverage/coverage-summary.json ]; then
node -e "
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
const total = data.total;
if (!total || total.lines === undefined) {
console.error('No total coverage data found');
process.exit(1);
}
const lines = total.lines.pct.toFixed(1);
const branches = total.branches.pct.toFixed(1);
const functions = total.functions.pct.toFixed(1);
const statements = total.statements.pct.toFixed(1);
const barLen = Math.round(total.lines.pct / 5);
const bar = '█'.repeat(barLen) + '░'.repeat(20 - barLen);
console.log('| 指标 | 覆盖率 |');
console.log('|------|--------|');
console.log('| 行覆盖率 | ' + bar + ' ' + lines + '% |');
console.log('| 分支覆盖率 | ' + branches + '% |');
console.log('| 函数覆盖率 | ' + functions + '% |');
console.log('| 语句覆盖率 | ' + statements + '% |');
console.log();
// Print per-file breakdown
console.log('| 文件 | 行覆盖率 |');
console.log('|------|----------|');
for (const [file, fileData] of Object.entries(data)) {
if (file === 'total') continue;
const fileLines = fileData.lines.pct.toFixed(1);
const fileBarLen = Math.round(fileData.lines.pct / 5);
const fileBar = '█'.repeat(fileBarLen) + '░'.repeat(20 - fileBarLen);
const shortPath = file.replace(/^.*src\//, 'src/');
console.log('| ' + shortPath + ' | ' + fileBar + ' ' + fileLines + '% |');
}
" >> "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
echo "> 详细报告见前端覆盖率 artifact" >> "$SUMMARY_FILE"
cat "$SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY"
else
echo "> ⚠️ 未生成前端覆盖率报告" >> "$SUMMARY_FILE"
echo "❌ 前端覆盖率生成失败:coverage-summary.json 未生成" >> "$SUMMARY_FILE"
echo "::error::前端覆盖率报告生成失败,检查 vitest 输出的测试失败信息"
cat "$SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
- name: 上传前端覆盖率摘要
uses: actions/upload-artifact@v4
if: always()
with:
name: frontend-coverage-summary
path: electron-app/frontend-coverage-summary.md
retention-days: 1
- name: 上传前端覆盖率报告
uses: actions/upload-artifact@v4
if: always()
with:
name: frontend-coverage
path: electron-app/coverage/
retention-days: 14
# ==================== Playwright E2E 测试 ====================
e2e-test:
name: Playwright E2E
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: 安装 pnpm
uses: pnpm/action-setup@v4
with:
version: "10"
- name: 安装依赖
working-directory: electron-app
run: pnpm install --frozen-lockfile
- name: 安装 Playwright 浏览器
working-directory: electron-app
run: npx playwright install chromium --with-deps
- name: 运行 Playwright E2E 测试
working-directory: electron-app
run: npx playwright test --config e2e/playwright.config.ts --update-snapshots missing
- name: 上传测试报告
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: electron-app/playwright-report/
retention-days: 7
- name: 上传测试截图
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-screenshots
path: electron-app/test-results/
retention-days: 7
# ==================== 安全检查 ====================
security:
name: 安全检查
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Rust 工具链
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: 安装系统依赖
run: sudo apt-get update && sudo apt-get install -y cmake clang
- name: Rust 依赖审计
if: always()
continue-on-error: false
run: |
if [ -f Cargo.lock ]; then
cargo install cargo-audit --locked
cargo audit || echo "::warning::Rust 依赖存在漏洞"
else
echo "::notice::未找到 Cargo.lock,跳过 Rust 依赖审计"
fi
- name: 检查硬编码密钥
run: |
echo "检查硬编码密钥..."
if grep -rE "(api[_-]?key|apikey|secret|password|token)\s*[=:]\s*['\"][^'\"]{10,}['\"]" \
--include="*.rs" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" \
--exclude-dir="node_modules" --exclude-dir="target" --exclude-dir="dist" \
--exclude-dir="coverage" --exclude-dir="tests" --exclude-dir="__tests__" \
--exclude="*.test.*" --exclude="*.spec.*" --exclude="*_test.*" --exclude="test_*" \
.; then
echo "::error::发现硬编码的密钥,请使用环境变量或配置文件加载"
exit 1
fi
echo "::notice::未发现硬编码密钥"
- name: 检查敏感文件
run: |
echo "检查敏感文件..."
SENSITIVE_FILES=(
".env"
".env.local"
".env.production"
"*.pem"
"*.key"
"secrets.json"
"credentials.json"
)
for file in "${SENSITIVE_FILES[@]}"; do
if ls $file 2>/dev/null | grep -q .; then
echo "::error::发现敏感文件: $file,请添加到.gitignore"
exit 1
fi
done
echo "::notice::未发现敏感文件"
# ==================== 统一测试脚本 (test.sh) ====================
unified-test:
name: 统一测试 (test.sh)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Rust ${{ env.RUST_VERSION }}
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: 安装 Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: 安装 pnpm
uses: pnpm/action-setup@v4
with:
version: "10"
- name: 安装前端依赖
working-directory: electron-app
run: pnpm install --frozen-lockfile
- name: 安装 Playwright 浏览器
working-directory: electron-app
run: npx playwright install chromium --with-deps
- name: 运行 test.sh
run: bash test.sh
# ==================== 发布模拟验证 ====================
publish-sim:
name: 发布模拟验证
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: npm pack
working-directory: packages/cli-box-skill
run: |
npm pack
ls -la cli-box-skill-*.tgz
- name: 验证 tarball 文件清单
working-directory: packages/cli-box-skill
run: |
TARBALL=$(ls cli-box-skill-*.tgz)
echo "=== 验证 $TARBALL ==="
tar tzf "$TARBALL" | sort
REQUIRED=("package/postinstall.mjs" "package/installer/cli.mjs" "package/installer/shared.mjs" "package/bin/cli-box-wrapper.js" "package/skill/SKILL.md")
for f in "${REQUIRED[@]}"; do
if tar tzf "$TARBALL" | grep -q "$f"; then
echo "✓ $f"
else
echo "✗ $f MISSING"
exit 1
fi
done
echo ""
echo "✅ tarball 文件清单验证通过"
- name: 验证包结构
working-directory: packages/cli-box-skill
run: |
echo "=== package.json 字段检查 ==="
node -e "
const pkg = require('./package.json');
const checks = [
['name', pkg.name === 'cli-box-skill'],
['bin.cli-box', !!pkg.bin?.['cli-box']],
['bin.cli-box-skill', !!pkg.bin?.['cli-box-skill']],
['files includes postinstall.mjs', pkg.files?.includes('postinstall.mjs')],
['files includes installer/', pkg.files?.some(f => f.startsWith('installer/'))],
['optionalDependencies.cli-box-darwin-arm64', !!pkg.optionalDependencies?.['cli-box-darwin-arm64']],
];
let ok = true;
for (const [name, pass] of checks) {
console.log(pass ? '✓ ' + name : '✗ ' + name);
if (!pass) ok = false;
}
if (!ok) process.exit(1);
console.log('\n✅ 包结构验证通过');
"
# ==================== 升级流程测试 (macOS) ====================
skill-upgrade-flow:
name: 升级流程测试
runs-on: macos-latest
timeout-minutes: 10
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: 安装当前 latest 版本
run: npm install -g cli-box-skill@latest
- name: 验证 latest 安装
run: |
echo "=== 验证 cli-box-skill 命令 ==="
cli-box-skill --help
echo ""
echo "=== 验证二进制链接 ==="
ls -la ~/.cli-box/bin/cli-box || echo "⚠ cli-box binary not found"
ls -la ~/.cli-box/bin/cli-box-daemon || echo "⚠ cli-box-daemon binary not found"
- name: 模拟用户自定义 SKILL.md
run: |
mkdir -p ~/.claude/skills/cli-box
echo "# Custom SKILL.md (user modified)" > ~/.claude/skills/cli-box/SKILL.md
echo "Custom content that should be preserved" >> ~/.claude/skills/cli-box/SKILL.md
echo ""
echo "=== 自定义 SKILL.md 内容 ==="
cat ~/.claude/skills/cli-box/SKILL.md
- name: 从 PR 分支打包
working-directory: packages/cli-box-skill
run: |
npm pack
ls -la cli-box-skill-*.tgz
- name: 运行 upgrade 命令
working-directory: packages/cli-box-skill
run: |
echo "=== 运行 upgrade(无 daemon)==="
node installer/cli.mjs upgrade --yes 2>&1 | tee /tmp/upgrade-output.log
- name: 验证升级结果
run: |
echo "============================================"
echo " 升级结果验证"
echo "============================================"
echo ""
echo "=== 二进制链接检查 ==="
if [ -L ~/.cli-box/bin/cli-box ]; then
echo "✓ cli-box is a symlink"
ls -la ~/.cli-box/bin/cli-box
else
echo "⚠ cli-box is NOT a symlink"
fi
if [ -L ~/.cli-box/bin/cli-box-daemon ]; then
echo "✓ cli-box-daemon is a symlink"
ls -la ~/.cli-box/bin/cli-box-daemon
else
echo "⚠ cli-box-daemon is NOT a symlink"
fi
echo ""
echo "=== SKILL.md 保留检查 ==="
if [ -f ~/.claude/skills/cli-box/SKILL.md ]; then
CONTENT=$(cat ~/.claude/skills/cli-box/SKILL.md)
if echo "$CONTENT" | grep -q "Custom content that should be preserved"; then
echo "✓ SKILL.md preserved (custom content intact)"
else
echo "✗ SKILL.md was overwritten!"
echo "Expected: 'Custom content that should be preserved'"
echo "Got:"
cat ~/.claude/skills/cli-box/SKILL.md
exit 1
fi
else
echo "⚠ SKILL.md not found at ~/.claude/skills/cli-box/SKILL.md"
fi
echo ""
echo "=== cli-box-skill 命令检查 ==="
cli-box-skill --help 2>&1 | head -5
- name: 上传升级日志
if: always()
uses: actions/upload-artifact@v4
with:
name: upgrade-output
path: /tmp/upgrade-output.log
retention-days: 7
# ==================== cli-box-skill 单元测试 ====================
skill-unit-test:
name: cli-box-skill 单元测试
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: 安装依赖
working-directory: packages/cli-box-skill
run: npm install
- name: 运行单元测试
working-directory: packages/cli-box-skill
run: node --test
# ==================== 门禁结果汇总 ====================
gate-result:
name: 门禁结果
runs-on: ubuntu-latest
needs: [rust-fmt, rust-clippy, rust-test, frontend-test, e2e-test, unified-test, security, publish-sim, skill-upgrade-flow, skill-unit-test]
if: always()
steps:
- name: 下载 Rust 覆盖率摘要
uses: actions/download-artifact@v4
if: always()
with:
name: rust-coverage-summary
path: coverage-artifacts
continue-on-error: true
- name: 下载前端覆盖率摘要
uses: actions/download-artifact@v4
if: always()
with:
name: frontend-coverage-summary
path: coverage-artifacts
continue-on-error: true
- name: 检查门禁结果
run: |
echo "============================================"
echo " cli-box CI 门禁结果"
echo "============================================"
echo ""
RUST_FMT="${{ needs.rust-fmt.result }}"
RUST_CLIPPY="${{ needs.rust-clippy.result }}"
RUST_TEST="${{ needs.rust-test.result }}"
FRONTEND_TEST="${{ needs.frontend-test.result }}"
E2E_TEST="${{ needs.e2e-test.result }}"
UNIFIED_TEST="${{ needs.unified-test.result }}"
SECURITY_RESULT="${{ needs.security.result }}"
PUBLISH_SIM="${{ needs.publish-sim.result }}"
UPGRADE_FLOW="${{ needs.skill-upgrade-flow.result }}"
SKILL_UNIT="${{ needs.skill-unit-test.result }}"
echo "| 检查项 | 状态 |"
echo "|-------|------|"
echo "| Rust 格式化 | $RUST_FMT |"
echo "| Rust Clippy | $RUST_CLIPPY |"
echo "| Rust 测试 & 覆盖率 | $RUST_TEST |"
echo "| 前端测试 & 覆盖率 | $FRONTEND_TEST |"
echo "| Playwright E2E | $E2E_TEST |"
echo "| 统一测试 (test.sh) | $UNIFIED_TEST |"
echo "| 安全检查 | $SECURITY_RESULT |"
echo "| 发布模拟验证 | $PUBLISH_SIM |"
echo "| 升级流程测试 | $UPGRADE_FLOW |"
echo "| cli-box-skill 单元测试 | $SKILL_UNIT |"
echo ""
if [ -f coverage-artifacts/rust-coverage-summary.md ]; then
echo "---"
cat coverage-artifacts/rust-coverage-summary.md
echo ""
fi
if [ -f coverage-artifacts/frontend-coverage-summary.md ]; then
echo "---"
cat coverage-artifacts/frontend-coverage-summary.md
echo ""
fi
if [[ "$RUST_FMT" == "success" && \
"$RUST_CLIPPY" == "success" && \
"$RUST_TEST" == "success" && \
"$FRONTEND_TEST" == "success" && \
"$E2E_TEST" == "success" && \
"$UNIFIED_TEST" == "success" && \
"$SECURITY_RESULT" == "success" && \
"$PUBLISH_SIM" == "success" && \
"$UPGRADE_FLOW" == "success" && \
"$SKILL_UNIT" == "success" ]]; then
echo "✅ 所有门禁检查通过!"
exit 0
else
echo "❌ 门禁检查未通过,请修复上述问题"
exit 1
fi
- name: PR 门禁状态评论
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const results = {
'Rust 格式化': '${{ needs.rust-fmt.result }}',
'Rust Clippy': '${{ needs.rust-clippy.result }}',
'Rust 测试 & 覆盖率': '${{ needs.rust-test.result }}',
'前端测试 & 覆盖率': '${{ needs.frontend-test.result }}',
'Playwright E2E': '${{ needs.e2e-test.result }}',
'统一测试 (test.sh)': '${{ needs.unified-test.result }}',
'安全检查': '${{ needs.security.result }}',
'发布模拟验证': '${{ needs.publish-sim.result }}',
'升级流程测试': '${{ needs.skill-upgrade-flow.result }}',
'cli-box-skill 单元测试': '${{ needs.skill-unit-test.result }}',
};
const statusEmoji = (status) => status === 'success' ? '✅' : (status === 'skipped' ? '⏭️' : '❌');
let tableRows = Object.entries(results).map(([name, status]) =>
`| ${name} | ${statusEmoji(status)} ${status} |`
).join('\n');
const allPassed = Object.values(results).every(r => r === 'success');
let coverageSection = '';
try {
const rustCov = fs.readFileSync('coverage-artifacts/rust-coverage-summary.md', 'utf8');
coverageSection += '\n' + rustCov + '\n';
} catch (e) {}
try {
const feCov = fs.readFileSync('coverage-artifacts/frontend-coverage-summary.md', 'utf8');
coverageSection += '\n' + feCov + '\n';
} catch (e) {}
const body = `## 🔒 门禁检查结果
| 检查项 | 状态 |
|-------|------|
${tableRows}
${coverageSection}
${allPassed
? '### ✅ 所有检查通过,可以合入\n\n> 点击 **Squash and merge** 合并此PR'
: '### ❌ 存在未通过的检查,请修复后重新提交'}
`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('🔒 门禁检查结果')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
}
# ==================== 发布构建验证 ====================
release-build:
name: 发布构建验证
runs-on: macos-latest
needs: [gate-result]
if: needs.gate-result.result == 'success'
timeout-minutes: 30
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Rust ${{ env.RUST_VERSION }}
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: 设置 Rust 缓存
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: 构建 CLI 二进制 (release)
run: cargo build -p cli-box-cli --release
- name: 整理构建产物
run: |
mkdir -p release
cp target/release/cli-box release/
echo "## 构建产物" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
ls -lh release/ | tail -n +2 | awk '{printf "| %s | %s |\n", $NF, $5}' >> "$GITHUB_STEP_SUMMARY"
- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: release-artifacts
path: release/
retention-days: 7