name: Secrets CLI - Build & Release on: push: branches: [main] paths: - 'src/**' - 'Cargo.toml' - 'Cargo.lock' - '.gitea/workflows/secrets.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: write env: BINARY_NAME: secrets 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' Cargo.toml | sed 's/.*"\(.*\)".*/\1/') tag="secrets-${version}" previous_tag=$(git tag --list 'secrets-*' --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: 创建 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 "${{ env.BINARY_NAME }} ${version}" \ --arg body "$body" \ '{tag_name: $tag, name: $name, body: $body}') 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 probe-runners: name: 探测 Runner runs-on: debian outputs: has_linux: ${{ steps.probe.outputs.has_linux }} has_macos: ${{ steps.probe.outputs.has_macos }} has_windows: ${{ steps.probe.outputs.has_windows }} steps: - name: 查询可用 Runner id: probe env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | if [ -z "$RELEASE_TOKEN" ]; then echo "has_linux=true" >> "$GITHUB_OUTPUT" echo "has_macos=true" >> "$GITHUB_OUTPUT" echo "has_windows=true" >> "$GITHUB_OUTPUT" exit 0 fi command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq) runners=$(curl -fsS -H "Authorization: token $RELEASE_TOKEN" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/runners" || true) if [ -z "$runners" ]; then echo "has_linux=true" >> "$GITHUB_OUTPUT" echo "has_macos=true" >> "$GITHUB_OUTPUT" echo "has_windows=true" >> "$GITHUB_OUTPUT" exit 0 fi has_runner() { local label="$1" printf '%s' "$runners" | jq -e --arg label "$label" ' (.runners // .data // . // []) | any( ( (.status // (if (.online // false) then "online" else "offline" end)) | ascii_downcase ) == "online" and ( (.labels // []) | map(if type == "object" then (.name // .label // "") else tostring end) | index($label) ) != null ) ' >/dev/null } if has_runner "debian"; then echo "has_linux=true" >> "$GITHUB_OUTPUT" else echo "has_linux=false" >> "$GITHUB_OUTPUT" fi if has_runner "darwin-arm64"; then echo "has_macos=true" >> "$GITHUB_OUTPUT" else echo "has_macos=false" >> "$GITHUB_OUTPUT" fi if has_runner "windows"; then echo "has_windows=true" >> "$GITHUB_OUTPUT" else echo "has_windows=false" >> "$GITHUB_OUTPUT" fi check: name: 质量检查 (fmt / clippy / test) runs-on: debian timeout-minutes: 10 steps: - name: 安装 Rust run: | if ! command -v cargo >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" fi source "$HOME/.cargo/env" 2>/dev/null || true rustup component add rustfmt clippy - 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-${{ hashFiles('Cargo.lock') }} restore-keys: | cargo-check- - run: cargo fmt -- --check - run: cargo clippy --locked -- -D warnings - run: cargo test --locked build-linux: name: Build (x86_64-unknown-linux-musl) needs: [version, probe-runners, check] if: needs.probe-runners.outputs.has_linux == 'true' runs-on: debian timeout-minutes: 10 continue-on-error: true steps: - name: 安装依赖 run: | sudo apt-get update sudo apt-get install -y pkg-config musl-tools binutils curl if ! command -v cargo >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable fi source "$HOME/.cargo/env" 2>/dev/null || true rustup target add x86_64-unknown-linux-musl echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - 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-${{ hashFiles('Cargo.lock') }} restore-keys: | cargo-x86_64-unknown-linux-musl- - run: cargo build --release --locked --target x86_64-unknown-linux-musl - run: strip target/x86_64-unknown-linux-musl/release/${{ env.BINARY_NAME }} - 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.BINARY_NAME }}" archive="${{ env.BINARY_NAME }}-${tag}-x86_64-linux-musl.tar.gz" tar -czf "$archive" -C "$(dirname "$bin")" "$(basename "$bin")" curl -fsS -H "Authorization: token $RELEASE_TOKEN" \ -F "attachment=@${archive}" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ needs.version.outputs.release_id }}/assets" build-macos: name: Build (aarch64-apple-darwin) needs: [version, probe-runners, check] if: needs.probe-runners.outputs.has_macos == 'true' runs-on: darwin-arm64 timeout-minutes: 10 continue-on-error: true steps: - name: 安装依赖 run: | if ! command -v cargo >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable fi source "$HOME/.cargo/env" 2>/dev/null || true rustup target add aarch64-apple-darwin echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - uses: actions/checkout@v4 - name: 缓存 Cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db target key: cargo-aarch64-apple-darwin-${{ hashFiles('Cargo.lock') }} restore-keys: | cargo-aarch64-apple-darwin- - run: cargo build --release --locked --target aarch64-apple-darwin - run: strip -x target/aarch64-apple-darwin/release/${{ env.BINARY_NAME }} - 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/aarch64-apple-darwin/release/${{ env.BINARY_NAME }}" archive="${{ env.BINARY_NAME }}-${tag}-aarch64-macos.tar.gz" tar -czf "$archive" -C "$(dirname "$bin")" "$(basename "$bin")" curl -fsS -H "Authorization: token $RELEASE_TOKEN" \ -F "attachment=@${archive}" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ needs.version.outputs.release_id }}/assets" build-windows: name: Build (x86_64-pc-windows-msvc) needs: [version, probe-runners, check] if: needs.probe-runners.outputs.has_windows == 'true' runs-on: windows timeout-minutes: 10 continue-on-error: true steps: - name: 安装依赖 shell: pwsh run: | if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile rustup-init.exe .\rustup-init.exe -y --default-toolchain stable Remove-Item rustup-init.exe } rustup target add x86_64-pc-windows-msvc - 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-pc-windows-msvc-${{ hashFiles('Cargo.lock') }} restore-keys: | cargo-x86_64-pc-windows-msvc- - name: 构建 shell: pwsh run: cargo build --release --locked --target x86_64-pc-windows-msvc - name: 上传 Release 产物 if: needs.version.outputs.release_id != '' shell: pwsh env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | if (-not $env:RELEASE_TOKEN) { exit 0 } $tag = "${{ needs.version.outputs.tag }}" $bin = "target\x86_64-pc-windows-msvc\release\${{ env.BINARY_NAME }}.exe" $archive = "${{ env.BINARY_NAME }}-${tag}-x86_64-windows.zip" Compress-Archive -Path $bin -DestinationPath $archive -Force $url = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ needs.version.outputs.release_id }}/assets" Invoke-RestMethod -Uri $url -Method Post ` -Headers @{ "Authorization" = "token $env:RELEASE_TOKEN" } ` -Form @{ attachment = Get-Item $archive } notify: name: 通知 needs: [version, probe-runners, check, build-linux, build-macos, build-windows] if: always() && github.event_name == 'push' runs-on: debian timeout-minutes: 1 continue-on-error: true steps: - uses: actions/checkout@v4 - name: 发送飞书通知 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=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "N/A") url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}" version_r="${{ needs.version.result }}" check_r="${{ needs.check.result }}" linux_r="${{ needs.build-linux.result }}" macos_r="${{ needs.build-macos.result }}" windows_r="${{ needs.build-windows.result }}" if [ "$version_r" = "success" ] && [ "$check_r" = "success" ]; then status="构建成功 ✅" else status="构建失败 ❌" fi icon() { case "$1" in success) echo "✅" ;; skipped) echo "⏭" ;; *) echo "❌" ;; esac } msg="${{ env.BINARY_NAME }} ${status}" if [ "$tag_exists" = "false" ]; then msg="${msg} 🆕 新版本 ${tag}" else msg="${msg} 🔄 重复构建 ${tag}" fi msg="${msg} 构建结果:linux$(icon "$linux_r") macOS$(icon "$macos_r") windows$(icon "$windows_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"