# MCP 分支:仅构建/发布 secrets-mcp(CLI 在 main 分支维护) name: Secrets MCP — Build & Release on: push: paths: - 'crates/**' - 'Cargo.toml' - 'Cargo.lock' # systemd / 部署模板变更也应跑构建(产物无变时可快速跳过 check) - 'deploy/**' - '.gitea/workflows/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: write env: MCP_BINARY: secrets-mcp RUST_TOOLCHAIN: 1.94.0 CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always RUST_BACKTRACE: short jobs: version: name: 版本 & Release runs-on: debian outputs: version: ${{ steps.ver.outputs.version }} tag: ${{ steps.ver.outputs.tag }} tag_exists: ${{ steps.ver.outputs.tag_exists }} release_id: ${{ steps.release.outputs.release_id }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: 解析版本 id: ver run: | version=$(grep -m1 '^version' crates/secrets-mcp/Cargo.toml | sed 's/.*"\(.*\)".*/\1/') tag="secrets-mcp-${version}" previous_tag=$(git tag --list 'secrets-mcp-*' --sort=-v:refname | awk -v tag="$tag" '$0 != tag { print; exit }') echo "version=${version}" >> "$GITHUB_OUTPUT" echo "tag=${tag}" >> "$GITHUB_OUTPUT" echo "previous_tag=${previous_tag}" >> "$GITHUB_OUTPUT" if git rev-parse "refs/tags/${tag}" >/dev/null 2>&1; then echo "tag_exists=true" >> "$GITHUB_OUTPUT" echo "版本 ${tag} 已存在" else echo "tag_exists=false" >> "$GITHUB_OUTPUT" echo "将创建新版本 ${tag}" fi - name: 严格拦截重复版本 if: steps.ver.outputs.tag_exists == 'true' run: | echo "错误: 版本 ${{ steps.ver.outputs.tag }} 已存在,禁止重复发版。" echo "请先 bump crates/secrets-mcp/Cargo.toml 中的 version,并执行 cargo build 同步 Cargo.lock。" exit 1 - name: 创建 Tag if: steps.ver.outputs.tag_exists == 'false' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git tag -a "${{ steps.ver.outputs.tag }}" -m "Release ${{ steps.ver.outputs.tag }}" git push origin "${{ steps.ver.outputs.tag }}" - name: 解析或创建 Release id: release env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | if [ -z "$RELEASE_TOKEN" ]; then echo "release_id=" >> "$GITHUB_OUTPUT" exit 0 fi command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq) tag="${{ steps.ver.outputs.tag }}" version="${{ steps.ver.outputs.version }}" release_api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" http_code=$(curl -sS -o /tmp/release.json -w '%{http_code}' \ -H "Authorization: token $RELEASE_TOKEN" \ "${release_api}/tags/${tag}") if [ "$http_code" = "200" ]; then release_id=$(jq -r '.id // empty' /tmp/release.json) if [ -n "$release_id" ]; then echo "已找到现有 Release: ${release_id}" echo "release_id=${release_id}" >> "$GITHUB_OUTPUT" exit 0 fi fi previous_tag="${{ steps.ver.outputs.previous_tag }}" if [ -n "$previous_tag" ]; then changes=$(git log --pretty=format:'- %s (%h)' "${previous_tag}..HEAD") else changes=$(git log --pretty=format:'- %s (%h)') fi [ -z "$changes" ] && changes="- 首次发布" body=$(printf '## 变更日志\n\n%s' "$changes") payload=$(jq -n \ --arg tag "$tag" \ --arg name "secrets-mcp ${version}" \ --arg body "$body" \ '{tag_name: $tag, name: $name, body: $body, draft: true}') http_code=$(curl -sS -o /tmp/create-release.json -w '%{http_code}' \ -H "Authorization: token $RELEASE_TOKEN" \ -H "Content-Type: application/json" \ -X POST "$release_api" \ -d "$payload") if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then release_id=$(jq -r '.id // empty' /tmp/create-release.json) fi if [ -n "$release_id" ]; then echo "已创建草稿 Release: ${release_id}" echo "release_id=${release_id}" >> "$GITHUB_OUTPUT" else echo "⚠ 创建 Release 失败 (HTTP ${http_code}),跳过产物上传" cat /tmp/create-release.json 2>/dev/null || true echo "release_id=" >> "$GITHUB_OUTPUT" fi check: name: 质量检查 (fmt / clippy / test) needs: [version] runs-on: debian timeout-minutes: 15 steps: - name: 安装 Rust run: | if ! command -v rustup >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain "${RUST_TOOLCHAIN}" echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" fi source "$HOME/.cargo/env" 2>/dev/null || true rustup toolchain install "${RUST_TOOLCHAIN}" --profile minimal --component rustfmt --component clippy rustup default "${RUST_TOOLCHAIN}" rustc -V cargo -V - uses: actions/checkout@v4 - name: 缓存 Cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db target key: cargo-check-${{ env.RUST_TOOLCHAIN }}-${{ hashFiles('Cargo.lock') }} restore-keys: | cargo-check-${{ env.RUST_TOOLCHAIN }}- cargo-check- - run: cargo fmt -- --check - run: cargo clippy --locked -- -D warnings - run: cargo test --locked build-linux: name: Build Linux (secrets-mcp, musl) needs: [version, check] runs-on: debian timeout-minutes: 25 steps: - name: 安装依赖 run: | sudo apt-get update sudo apt-get install -y pkg-config musl-tools binutils curl if ! command -v rustup >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain "${RUST_TOOLCHAIN}" echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" fi source "$HOME/.cargo/env" 2>/dev/null || true rustup toolchain install "${RUST_TOOLCHAIN}" --profile minimal rustup default "${RUST_TOOLCHAIN}" rustup target add x86_64-unknown-linux-musl --toolchain "${RUST_TOOLCHAIN}" rustc -V cargo -V - uses: actions/checkout@v4 - name: 缓存 Cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db target key: cargo-x86_64-unknown-linux-musl-${{ env.RUST_TOOLCHAIN }}-${{ hashFiles('Cargo.lock') }} restore-keys: | cargo-x86_64-unknown-linux-musl-${{ env.RUST_TOOLCHAIN }}- cargo-x86_64-unknown-linux-musl- - name: 构建 secrets-mcp (musl) run: | cargo build --release --locked --target x86_64-unknown-linux-musl -p secrets-mcp strip target/x86_64-unknown-linux-musl/release/${{ env.MCP_BINARY }} - name: 上传 Release 产物 if: needs.version.outputs.release_id != '' env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | [ -z "$RELEASE_TOKEN" ] && exit 0 tag="${{ needs.version.outputs.tag }}" bin="target/x86_64-unknown-linux-musl/release/${{ env.MCP_BINARY }}" archive="${{ env.MCP_BINARY }}-${tag}-x86_64-linux-musl.tar.gz" tar -czf "$archive" -C "$(dirname "$bin")" "$(basename "$bin")" sha256sum "$archive" > "${archive}.sha256" release_url="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ needs.version.outputs.release_id }}/assets" curl -fsS -H "Authorization: token $RELEASE_TOKEN" \ -F "attachment=@${archive}" "$release_url" curl -fsS -H "Authorization: token $RELEASE_TOKEN" \ -F "attachment=@${archive}.sha256" "$release_url" - name: 飞书通知 if: always() env: WEBHOOK_URL: ${{ vars.WEBHOOK_URL }} run: | [ -z "$WEBHOOK_URL" ] && exit 0 command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq) tag="${{ needs.version.outputs.tag }}" commit=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "N/A") url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}" result="${{ job.status }}" if [ "$result" = "success" ]; then icon="✅"; else icon="❌"; fi msg="secrets-mcp linux 构建${icon} 版本:${tag} 提交:${commit} 作者:${{ github.actor }} 详情:${url}" payload=$(jq -n --arg text "$msg" '{msg_type: "text", content: {text: $text}}') curl -sS -H "Content-Type: application/json" -X POST -d "$payload" "$WEBHOOK_URL" deploy-mcp: name: 部署 secrets-mcp needs: [version, build-linux] # 部署目标由仓库 Actions 配置:vars.DEPLOY_HOST / vars.DEPLOY_USER;私钥 secrets.DEPLOY_SSH_KEY(PEM 原文,勿 base64) # (可用 scripts/setup-gitea-actions.sh 或 Gitea API 写入,勿写进本文件) # Google OAuth / SERVER_MASTER_KEY / SECRETS_DATABASE_URL 等勿写入 CI,请在 ECS 上 # /opt/secrets-mcp/.env 配置(见 deploy/.env.example)。 # 若仓库 main 仍为纯 CLI、仅 feat/mcp 含本 workflow,请去掉条件里的 main,避免误部署。 if: needs.build-linux.result == 'success' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feat/mcp' || github.ref == 'refs/heads/mcp') runs-on: debian timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: 安装 Rust run: | if ! command -v rustup >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain "${RUST_TOOLCHAIN}" echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" fi source "$HOME/.cargo/env" 2>/dev/null || true sudo apt-get update -qq && sudo apt-get install -y -qq pkg-config musl-tools rustup toolchain install "${RUST_TOOLCHAIN}" --profile minimal rustup default "${RUST_TOOLCHAIN}" rustup target add x86_64-unknown-linux-musl --toolchain "${RUST_TOOLCHAIN}" rustc -V cargo -V - name: 缓存 Cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db target key: cargo-x86_64-unknown-linux-musl-${{ env.RUST_TOOLCHAIN }}-${{ hashFiles('Cargo.lock') }} restore-keys: | cargo-x86_64-unknown-linux-musl-${{ env.RUST_TOOLCHAIN }}- cargo-x86_64-unknown-linux-musl- - name: 构建 secrets-mcp run: | cargo build --release --locked --target x86_64-unknown-linux-musl -p secrets-mcp strip target/x86_64-unknown-linux-musl/release/${{ env.MCP_BINARY }} - name: 部署到阿里云 ECS env: DEPLOY_HOST: ${{ vars.DEPLOY_HOST }} DEPLOY_USER: ${{ vars.DEPLOY_USER }} DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} run: | if [ -z "$DEPLOY_HOST" ] || [ -z "$DEPLOY_USER" ] || [ -z "$DEPLOY_SSH_KEY" ]; then echo "部署跳过:请在仓库 Actions 中配置 vars.DEPLOY_HOST、vars.DEPLOY_USER 与 secrets.DEPLOY_SSH_KEY" exit 0 fi echo "$DEPLOY_SSH_KEY" > /tmp/deploy_key chmod 600 /tmp/deploy_key SCP="scp -i /tmp/deploy_key -o StrictHostKeyChecking=no" $SCP target/x86_64-unknown-linux-musl/release/${{ env.MCP_BINARY }} \ "${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/secrets-mcp.new" ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no "${DEPLOY_USER}@${DEPLOY_HOST}" " sudo mv /tmp/secrets-mcp.new /opt/secrets-mcp/secrets-mcp sudo chmod +x /opt/secrets-mcp/secrets-mcp sudo systemctl restart secrets-mcp sleep 2 sudo systemctl is-active secrets-mcp && echo '服务启动成功' || (sudo journalctl -u secrets-mcp -n 20 && exit 1) " rm -f /tmp/deploy_key - name: 飞书通知 if: always() env: WEBHOOK_URL: ${{ vars.WEBHOOK_URL }} run: | [ -z "$WEBHOOK_URL" ] && exit 0 command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq) tag="${{ needs.version.outputs.tag }}" commit=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "N/A") url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}" result="${{ job.status }}" if [ "$result" = "success" ]; then icon="✅"; else icon="❌"; fi msg="secrets-mcp 部署${icon} 版本:${tag} 提交:${commit} 作者:${{ github.actor }} 详情:${url}" payload=$(jq -n --arg text "$msg" '{msg_type: "text", content: {text: $text}}') curl -sS -H "Content-Type: application/json" -X POST -d "$payload" "$WEBHOOK_URL" publish-release: name: 发布草稿 Release needs: [version, build-linux] if: always() && needs.version.outputs.release_id != '' runs-on: debian timeout-minutes: 5 steps: - uses: actions/checkout@v4 - name: 发布草稿 env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | [ -z "$RELEASE_TOKEN" ] && exit 0 linux_r="${{ needs.build-linux.result }}" if [ "$linux_r" != "success" ]; then echo "linux 构建未成功,保留草稿 Release" exit 0 fi release_api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ needs.version.outputs.release_id }}" http_code=$(curl -sS -o /tmp/publish-release.json -w '%{http_code}' \ -H "Authorization: token $RELEASE_TOKEN" \ -H "Content-Type: application/json" \ -X PATCH "$release_api" \ -d '{"draft":false}') if [ "$http_code" != "200" ]; then echo "发布草稿 Release 失败 (HTTP ${http_code})" cat /tmp/publish-release.json 2>/dev/null || true exit 1 fi echo "Release 已发布" - name: 飞书汇总通知 if: always() env: WEBHOOK_URL: ${{ vars.WEBHOOK_URL }} run: | [ -z "$WEBHOOK_URL" ] && exit 0 command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq) tag="${{ needs.version.outputs.tag }}" tag_exists="${{ needs.version.outputs.tag_exists }}" commit="${{ github.event.head_commit.message }}" [ -z "$commit" ] && commit="${{ github.sha }}" url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}" linux_r="${{ needs.build-linux.result }}" publish_r="${{ job.status }}" icon() { case "$1" in success) echo "✅";; skipped) echo "⏭";; *) echo "❌";; esac; } if [ "$linux_r" = "success" ] && [ "$publish_r" = "success" ]; then status="发布成功 ✅" elif [ "$linux_r" != "success" ]; then status="构建失败 ❌" else status="发布失败 ❌" fi if [ "$tag_exists" = "false" ]; then version_line="🆕 新版本 ${tag}" else version_line="🔄 重复构建 ${tag}" fi msg="secrets-mcp ${status} ${version_line} linux $(icon "$linux_r") | Release $(icon "$publish_r") 提交:${commit} 作者:${{ github.actor }} 详情:${url}" payload=$(jq -n --arg text "$msg" '{msg_type: "text", content: {text: $text}}') curl -sS -H "Content-Type: application/json" -X POST -d "$payload" "$WEBHOOK_URL"