# MCP 分支:仅构建/发布 secrets-mcp(CLI 在 main 分支维护) name: Secrets MCP — Build & Release on: push: paths: - 'crates/**' - 'Cargo.toml' - 'Cargo.lock' - '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 MUSL_TARGET: x86_64-unknown-linux-musl jobs: ci: name: 检查 / 构建 / 发版 runs-on: debian timeout-minutes: 40 outputs: tag: ${{ steps.ver.outputs.tag }} version: ${{ steps.ver.outputs.version }} 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}" echo "version=${version}" >> "$GITHUB_OUTPUT" echo "tag=${tag}" >> "$GITHUB_OUTPUT" # 版本 bump 硬检查:若本次推送包含 crates/ 或 Cargo.toml 变更, # 但版本号与上一提交一致,则视为未发版,直接失败。 prev_version=$(git show HEAD^:crates/secrets-mcp/Cargo.toml 2>/dev/null | grep -m1 '^version' | sed 's/.*"\(.*\)".*/\1/' || true) if [ -n "$prev_version" ] && [ "$version" = "$prev_version" ]; then # 确认本次推送是否包含 crates/ 或 Cargo.toml 变更 if git diff --name-only HEAD^ HEAD 2>/dev/null | grep -qE '^crates/|^Cargo\.toml$'; then echo "::error::工作区包含 crates/ 或 Cargo.toml 变更,但版本号未 bump(${version} == ${prev_version})" echo "按规则,每次代码变更必须 bump crates/secrets-mcp/Cargo.toml 中的 version。" exit 1 fi fi if git rev-parse "refs/tags/${tag}" >/dev/null 2>&1; then echo "⚠ 版本 ${tag} 已存在,将覆盖重新发版。" echo "tag_exists=true" >> "$GITHUB_OUTPUT" else echo "将创建新版本 ${tag}" echo "tag_exists=false" >> "$GITHUB_OUTPUT" fi # ── Rust 工具链 ────────────────────────────────────────────────────── - name: 安装 Rust 与 musl 工具链 run: | sudo apt-get update -qq sudo apt-get install -y -qq pkg-config musl-tools binutils jq 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}" rustup target add "${MUSL_TARGET}" --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-${{ env.MUSL_TARGET }}-${{ env.RUST_TOOLCHAIN }}-${{ hashFiles('Cargo.lock') }} restore-keys: | cargo-${{ env.MUSL_TARGET }}-${{ env.RUST_TOOLCHAIN }}- cargo-${{ env.MUSL_TARGET }}- # ── 质量检查(先于构建,失败即止)────────────────────────────────── - name: fmt run: cargo fmt -- --check - name: clippy run: cargo clippy --locked -- -D warnings - name: test run: cargo test --locked # ── 构建(质量检查通过后才执行)──────────────────────────────────── - name: 构建 secrets-mcp (musl) run: | cargo build --release --locked --target "${MUSL_TARGET}" -p secrets-mcp strip "target/${MUSL_TARGET}/release/${MCP_BINARY}" - name: 上传构建产物 uses: actions/upload-artifact@v3 with: name: ${{ env.MCP_BINARY }}-linux-musl path: target/${{ env.MUSL_TARGET }}/release/${{ env.MCP_BINARY }} retention-days: 3 # ── 创建 / 覆盖 Tag(构建成功后才打)─────────────────────────────── - name: 创建 Tag run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" tag="${{ steps.ver.outputs.tag }}" if [ "${{ steps.ver.outputs.tag_exists }}" = "true" ]; then git tag -d "$tag" 2>/dev/null || true git push origin ":refs/tags/$tag" 2>/dev/null || true fi git tag -a "$tag" -m "Release $tag" git push origin "$tag" # ── Release(可选,需配置 RELEASE_TOKEN)─────────────────────────── - name: Upsert Release if: env.RELEASE_TOKEN != '' env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | tag="${{ steps.ver.outputs.tag }}" version="${{ steps.ver.outputs.version }}" api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" auth="Authorization: token $RELEASE_TOKEN" previous_tag=$(git tag --list 'secrets-mcp-*' --sort=-v:refname | awk -v t="$tag" '$0 != t { print; exit }') 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") # Upsert: 存在 → PATCH + 清旧 assets;不存在 → POST release_id=$(curl -sS -H "$auth" "${api}/tags/${tag}" 2>/dev/null | jq -r '.id // empty') if [ -n "$release_id" ]; then curl -sS -o /dev/null -H "$auth" -H "Content-Type: application/json" \ -X PATCH "${api}/${release_id}" \ -d "$(jq -n --arg n "secrets-mcp ${version}" --arg b "$body" '{name:$n,body:$b,draft:false}')" curl -sS -H "$auth" "${api}/${release_id}/assets" | \ jq -r '.[].id' | xargs -I{} curl -sS -o /dev/null -H "$auth" -X DELETE "${api}/${release_id}/assets/{}" echo "已更新 Release ${release_id}" else release_id=$(curl -fsS -H "$auth" -H "Content-Type: application/json" \ -X POST "$api" \ -d "$(jq -n --arg t "$tag" --arg n "secrets-mcp ${version}" --arg b "$body" \ '{tag_name:$t,name:$n,body:$b,draft:false}')" | jq -r '.id') echo "已创建 Release ${release_id}" fi bin="target/${MUSL_TARGET}/release/${MCP_BINARY}" archive="${MCP_BINARY}-${tag}-x86_64-linux-musl.tar.gz" tar -czf "$archive" -C "$(dirname "$bin")" "$(basename "$bin")" sha256sum "$archive" > "${archive}.sha256" curl -fsS -H "$auth" -F "attachment=@${archive}" "${api}/${release_id}/assets" curl -fsS -H "$auth" -F "attachment=@${archive}.sha256" "${api}/${release_id}/assets" echo "Release ${tag} 已发布" # ── 飞书汇总通知 ───────────────────────────────────────────────────── - name: 飞书通知 if: always() env: WEBHOOK_URL: ${{ vars.WEBHOOK_URL }} run: | [ -z "$WEBHOOK_URL" ] && exit 0 tag="${{ steps.ver.outputs.tag }}" commit="${{ github.event.head_commit.message }}" [ -z "$commit" ] && commit="${{ github.sha }}" 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" deploy: name: 部署 secrets-mcp needs: [ci] if: | github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feat/mcp' || github.ref == 'refs/heads/mcp' runs-on: debian timeout-minutes: 10 steps: - name: 下载构建产物 uses: actions/download-artifact@v3 with: name: ${{ env.MCP_BINARY }}-linux-musl path: /tmp/artifact - name: 部署到阿里云 ECS env: DEPLOY_HOST: ${{ vars.DEPLOY_HOST }} DEPLOY_USER: ${{ vars.DEPLOY_USER }} DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} DEPLOY_KNOWN_HOSTS: ${{ vars.DEPLOY_KNOWN_HOSTS }} run: | if [ -z "$DEPLOY_HOST" ] || [ -z "$DEPLOY_USER" ] || [ -z "$DEPLOY_SSH_KEY" ]; then echo "部署跳过:请配置 vars.DEPLOY_HOST、vars.DEPLOY_USER 与 secrets.DEPLOY_SSH_KEY" exit 0 fi install -m 600 /dev/null /tmp/deploy_key echo "$DEPLOY_SSH_KEY" > /tmp/deploy_key trap 'rm -f /tmp/deploy_key' EXIT if [ -n "$DEPLOY_KNOWN_HOSTS" ]; then echo "$DEPLOY_KNOWN_HOSTS" > /tmp/deploy_known_hosts ssh_opts="-o UserKnownHostsFile=/tmp/deploy_known_hosts -o StrictHostKeyChecking=yes" else ssh_opts="-o StrictHostKeyChecking=accept-new" fi scp -i /tmp/deploy_key $ssh_opts \ "/tmp/artifact/${MCP_BINARY}" \ "${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/secrets-mcp.new" ssh -i /tmp/deploy_key $ssh_opts "${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) " - name: 飞书通知 if: always() env: WEBHOOK_URL: ${{ vars.WEBHOOK_URL }} run: | [ -z "$WEBHOOK_URL" ] && exit 0 tag="${{ needs.ci.outputs.tag }}" 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} 作者:${{ 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"