mcpField
D
dotnet-cli-release-pipeline

dotnet-cli-release-pipeline

github.com/wshaddix云与 DevOps8670 次安装35 周前更新
代码仓库

关于 dotnet-cli-release-pipeline

面向 .NET CLI 工具维护者的发布流水线技能,说明如何以 Git 标签为触发点构建并发布多个分发格式。内容涵盖 RID 构建矩阵、校验和,以及自动创建 Homebrew、winget 和 Scoop 更新请求。

主要功能

  1. 01Git 标签与 SemVer 版本流程
  2. 02按 RID 构建矩阵生成制品
  3. 03跨作业暂存和汇集制品
  4. 04GitHub Release 与 SHA-256 校验和
  5. 05自动提交 Homebrew、winget 和 Scoop 更新
  6. 06预发布版本的分发门控

适用场景

  1. 01为 .NET CLI 版本标签配置发布工作流
  2. 02为多种 RID 生成发布文件
  3. 03为发布资产生成校验和
  4. 04自动向包管理器仓库提交版本更新

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-cli-release-pipeline

安装后在 Claude Code 中输入/dotnet-cli-release-pipeline确认已加载。

详细介绍

dotnet-cli-release-pipeline

面向 .NET CLI 工具的统一发布 CI/CD 流水线:由单个版本标签触发 GitHub Actions 工作流,为每个运行时标识符 (RID) 使用构建矩阵,在作业之间暂存构件,创建带 SHA-256 校验和的 GitHub Releases,自动创建 Homebrew formula 和 winget manifest PR,并使用 git 标签实施 SemVer 版本策略。

**版本假设:**以 .NET 8.0+ 为基准。GitHub Actions 工作流语法 v2。这些模式适用于任何 CI 系统,但示例使用 GitHub Actions。

**范围边界:**此技能负责 CLI 专用发布流水线,即 CLI 工具构件的构建、打包和发布工作流。通用 CI/CD 模式(分支保护、矩阵测试策略、部署流水线、可复用工作流)请参阅 [skill:dotnet-gha-patterns] 和 [skill:dotnet-ado-patterns]。此技能侧重于从单个触发器向多个包管理器发布 CLI 二进制文件的独特要求。

**范围外内容:**通用 CI/CD 模式(分支策略、矩阵测试、部署流水线)请参阅 [skill:dotnet-gha-patterns] 和 [skill:dotnet-ado-patterns]。Native AOT 编译配置请参阅 [skill:dotnet-native-aot]。分发策略决策请参阅 [skill:dotnet-cli-distribution]。包格式详情请参阅 [skill:dotnet-cli-packaging]。容器映像发布请参阅 [skill:dotnet-containers]。

交叉引用:[skill:dotnet-cli-distribution](RID 矩阵和发布策略)、[skill:dotnet-cli-packaging](包格式编写)、[skill:dotnet-native-aot](AOT 发布配置)、[skill:dotnet-containers](基于容器的分发)。


版本策略

SemVer + Git 标签

使用语义化版本 (SemVer),并以 git 标签作为发布版本的唯一真实来源。

标签格式:v{major}.{minor}.{patch}(例如,v1.2.3)

# Tag a release
git tag -a v1.2.3 -m "Release v1.2.3"
git push origin v1.2.3
版本流转
git tag v1.2.3
    │
    ▼
GitHub Actions trigger (on push tags: v*)
    │
    ▼
Extract version from tag: GITHUB_REF_NAME → v1.2.3 → 1.2.3
    │
    ▼
Pass to dotnet publish /p:Version=1.2.3
    │
    ▼
Embed in binary (--version output)
    │
    ▼
Stamp in package manifests (Homebrew, winget, Scoop, NuGet)
从标签提取版本
- name: Extract version from tag
  id: version
  run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
  # v1.2.3 → 1.2.3
预发布版本
# Pre-release tag
git tag -a v1.3.0-rc.1 -m "Release candidate 1"

# CI detects pre-release and skips package manager submissions
# but still creates GitHub Release as pre-release

统一的 GitHub Actions 工作流

完整工作流
name: Release

on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+*"  # v1.2.3, v1.2.3-rc.1

permissions:
  contents: write  # Create GitHub Releases

defaults:
  run:
    shell: bash

env:
  PROJECT: src/MyCli/MyCli.csproj
  DOTNET_VERSION: "8.0.x"

jobs:
  build:
    strategy:
      matrix:
        include:
          - rid: linux-x64
            os: ubuntu-latest
          - rid: linux-arm64
            os: ubuntu-latest
          - rid: osx-arm64
            os: macos-latest
          - rid: win-x64
            os: windows-latest
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Extract version
        id: version
        shell: bash
        run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

      - name: Publish
        run: >-
          dotnet publish ${{ env.PROJECT }}
          -c Release
          -r ${{ matrix.rid }}
          -o ./publish
          /p:Version=${{ steps.version.outputs.version }}

      - name: Package (Unix)
        if: runner.os != 'Windows'
        run: |
          set -euo pipefail
          cd publish
          tar -czf "$GITHUB_WORKSPACE/mytool-${{ steps.version.outputs.version }}-${{ matrix.rid }}.tar.gz" .

      - name: Package (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          Compress-Archive -Path "publish/*" `
            -DestinationPath "mytool-${{ steps.version.outputs.version }}-${{ matrix.rid }}.zip"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: release-${{ matrix.rid }}
          path: |
            *.tar.gz
            *.zip

  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Extract version
        id: version
        run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          merge-multiple: true

      - name: Generate checksums
        working-directory: artifacts
        run: |
          set -euo pipefail
          shasum -a 256 *.tar.gz *.zip > checksums-sha256.txt
          cat checksums-sha256.txt

      - name: Detect pre-release
        id: prerelease
        run: |
          set -euo pipefail
          if [[ "${{ steps.version.outputs.version }}" == *-* ]]; then
            echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
          else
            echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
          fi

      # Pin third-party actions to a commit SHA in production for supply-chain security
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          name: v${{ steps.version.outputs.version }}
          prerelease: ${{ steps.prerelease.outputs.is_prerelease }}
          generate_release_notes: true
          files: |
            artifacts/*.tar.gz
            artifacts/*.zip
            artifacts/checksums-sha256.txt

  publish-nuget:
    needs: release
    if: ${{ !contains(github.ref_name, '-') }}  # Skip pre-releases
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Extract version
        id: version
        run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

      - name: Pack
        run: >-
          dotnet pack ${{ env.PROJECT }}
          -c Release
          /p:Version=${{ steps.version.outputs.version }}
          -o ./nupkgs

      - name: Push to NuGet
        run: >-
          dotnet nuget push ./nupkgs/*.nupkg
          --source https://api.nuget.org/v3/index.json
          --api-key ${{ secrets.NUGET_API_KEY }}

按 RID 构建矩阵

矩阵策略

构建矩阵为每个 RID 生成一个构件。每个 RID 都在相应的运行器操作系统上运行。

strategy:
  matrix:
    include:
      - rid: linux-x64
        os: ubuntu-latest
      - rid: linux-arm64
        os: ubuntu-latest        # Cross-compile ARM64 on x64 runner
      - rid: osx-arm64
        os: macos-latest         # Native ARM64 runner
      - rid: win-x64
        os: windows-latest
交叉编译说明
  • ubuntu-latest 上的 linux-arm64:.NET 支持对托管(非 AOT)构建进行交叉编译。在 x64 运行器上执行 dotnet publish -r linux-arm64,无需 QEMU 即可生成有效的 ARM64 二进制文件。对于 Native AOT,在 x64 运行器上交叉编译 ARM64 需要 ARM64 交叉编译工具链(gcc-aarch64-linux-gnu 或等效工具)。交叉编译的前置条件请参阅 [skill:dotnet-native-aot]。
  • **osx-arm64:**使用 macos-latest(提供 ARM64 运行器)进行原生编译。不支持从 Linux 交叉编译 macOS ARM64。
  • **windows-latest 上的 win-x64:**在 Windows 运行器上进行原生编译。
扩展矩阵(可选)
strategy:
  matrix:
    include:
      # Primary targets
      - rid: linux-x64
        os: ubuntu-latest
      - rid: linux-arm64
        os: ubuntu-latest
      - rid: osx-arm64
        os: macos-latest
      - rid: win-x64
        os: windows-latest
      # Extended targets
      - rid: osx-x64
        os: macos-13           # Intel macOS runner
      - rid: linux-musl-x64
        os: ubuntu-latest      # Alpine musl cross-compile

构件暂存

上传每个 RID 的构件

每个矩阵作业都会使用特定于 RID 的名称上传其构件:

- name: Upload artifact
  uses: actions/upload-artifact@v4
  with:
    name: release-${{ matrix.rid }}
    path: |
      *.tar.gz
      *.zip
    retention-days: 1  # Short retention -- artifacts are published to GitHub Releases
在发布作业中下载

发布作业会下载构建矩阵中的所有构件:

- name: Download all artifacts
  uses: actions/download-artifact@v4
  with:
    path: artifacts
    merge-multiple: true  # Merge all release-* artifacts into one directory

下载后,artifacts/ 中包含:

artifacts/
  mytool-1.2.3-linux-x64.tar.gz
  mytool-1.2.3-linux-arm64.tar.gz
  mytool-1.2.3-osx-arm64.tar.gz
  mytool-1.2.3-win-x64.zip

使用校验和创建 GitHub Releases

生成校验和
- name: Generate checksums
  working-directory: artifacts
  run: |
    set -euo pipefail
    shasum -a 256 *.tar.gz *.zip > checksums-sha256.txt
    cat checksums-sha256.txt

输出格式 (checksums-sha256.txt):

abc123...  mytool-1.2.3-linux-x64.tar.gz
def456...  mytool-1.2.3-linux-arm64.tar.gz
ghi789...  mytool-1.2.3-osx-arm64.tar.gz
jkl012...  mytool-1.2.3-win-x64.zip
创建 Release
- name: Create GitHub Release
  uses: softprops/action-gh-release@v2
  with:
    name: v${{ steps.version.outputs.version }}
    prerelease: ${{ steps.prerelease.outputs.is_prerelease }}
    generate_release_notes: true
    files: |
      artifacts/*.tar.gz
      artifacts/*.zip
      artifacts/checksums-sha256.txt

generate_release_notes: true 会根据自上一个标签以来合并的 PR 和提交消息自动生成发布说明。


自动创建 Formula/Manifest PR

更新 Homebrew Formula

发布 GitHub Release 后,自动更新 Homebrew tap:

  update-homebrew:
    needs: release
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: ubuntu-latest
    steps:
      - name: Extract version
        id: version
        run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

      - uses: actions/checkout@v4
        with:
          repository: myorg/homebrew-tap
          token: ${{ secrets.TAP_GITHUB_TOKEN }}

      - name: Download checksums
        run: |
          set -euo pipefail
          curl -sL "https://github.com/myorg/mytool/releases/download/v${{ steps.version.outputs.version }}/checksums-sha256.txt" \
            -o checksums.txt

      - name: Update formula
        run: |
          set -euo pipefail
          VERSION="${{ steps.version.outputs.version }}"
          LINUX_X64_SHA=$(grep "linux-x64" checksums.txt | awk '{print $1}')
          LINUX_ARM64_SHA=$(grep "linux-arm64" checksums.txt | awk '{print $1}')
          OSX_ARM64_SHA=$(grep "osx-arm64" checksums.txt | awk '{print $1}')

          # Use sed or a templating script to update Formula/mytool.rb
          # with new version and SHA-256 values
          python3 scripts/update-formula.py \
            --version "$VERSION" \
            --linux-x64-sha "$LINUX_X64_SHA" \
            --linux-arm64-sha "$LINUX_ARM64_SHA" \
            --osx-arm64-sha "$OSX_ARM64_SHA"

      - name: Create PR
        uses: peter-evans/create-pull-request@v6
        with:
          title: "mytool ${{ steps.version.outputs.version }}"
          commit-message: "Update mytool to ${{ steps.version.outputs.version }}"
          branch: "update-mytool-${{ steps.version.outputs.version }}"
          body: |
            Automated update for mytool v${{ steps.version.outputs.version }}
            Release: https://github.com/myorg/mytool/releases/tag/v${{ steps.version.outputs.version }}
更新 winget Manifest
  update-winget:
    needs: release
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: windows-latest
    steps:
      - name: Extract version
        id: version
        shell: bash
        run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

      - name: Submit to winget-pkgs
        uses: vedantmgoyal9/winget-releaser@main
        with:
          identifier: MyOrg.MyTool
          version: ${{ steps.version.outputs.version }}
          installers-regex: '\.zip$'
          token: ${{ secrets.WINGET_GITHUB_TOKEN }}
更新 Scoop Manifest
  update-scoop:
    needs: release
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: ubuntu-latest
    steps:
      - name: Extract version
        id: version
        run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

      - uses: actions/checkout@v4
        with:
          repository: myorg/scoop-mytool
          token: ${{ secrets.SCOOP_GITHUB_TOKEN }}

      - name: Download checksums
        run: |
          set -euo pipefail
          curl -sL "https://github.com/myorg/mytool/releases/download/v${{ steps.version.outputs.version }}/checksums-sha256.txt" \
            -o checksums.txt

      - name: Update manifest
        run: |
          set -euo pipefail
          VERSION="${{ steps.version.outputs.version }}"
          WIN_X64_SHA=$(grep "win-x64" checksums.txt | awk '{print $1}')

          # Update bucket/mytool.json with new version and hash
          jq --arg v "$VERSION" --arg h "$WIN_X64_SHA" \
            '.version = $v | .architecture."64bit".hash = $h |
             .architecture."64bit".url = "https://github.com/myorg/mytool/releases/download/v\($v)/mytool-\($v)-win-x64.zip"' \
            bucket/mytool.json > tmp.json && mv tmp.json bucket/mytool.json

      - name: Create PR
        uses: peter-evans/create-pull-request@v6
        with:
          title: "mytool ${{ steps.version.outputs.version }}"
          commit-message: "Update mytool to ${{ steps.version.outputs.version }}"
          branch: "update-mytool-${{ steps.version.outputs.version }}"

版本策略详情

CLI 工具的 SemVer
变更类型版本递增示例
CLI 标志重命名或移除,造成不兼容变更主版本1.x.x -> 2.0.0
新增命令或选项次版本x.1.x -> x.2.0
Bug 修复、性能改进补丁版本x.x.1 -> x.x.2
发布候选版预发布后缀x.x.x-rc.1
版本嵌入

版本从 git 标签一路传递,经由 dotnet publish 写入二进制文件:


<PropertyGroup>
  
  <Version>0.0.0-dev</Version>
</PropertyGroup>
# --version output matches the git tag
$ mytool --version
1.2.3
打标签工作流
# 1. Update CHANGELOG.md (if applicable)
# 2. Commit the changelog
git commit -am "docs: update changelog for v1.2.3"

# 3. Tag the release
git tag -a v1.2.3 -m "Release v1.2.3"

# 4. Push tag -- triggers the release workflow
git push origin v1.2.3

工作流安全

密钥管理
# Required repository secrets:
# NUGET_API_KEY         - NuGet.org API key for package publishing
# TAP_GITHUB_TOKEN      - PAT with repo scope for homebrew-tap
# WINGET_GITHUB_TOKEN   - PAT with public_repo scope for winget-pkgs PRs
# SCOOP_GITHUB_TOKEN    - PAT with repo scope for scoop bucket
# CHOCO_API_KEY         - Chocolatey API key for package push
权限
permissions:
  contents: write  # Minimum: create GitHub Releases and upload assets

如果不同作业需要不同的权限范围,请使用作业级权限。切勿授予 write-all。


Agent 注意事项

  1. 在 GitHub Actions 的 bash 步骤中,不要在未设置 set -o pipefail 的情况下使用 set -e。 如果未设置 pipefail,通过管道传递给 tee 或其他实用工具的失败命令会以 0 退出,从而掩盖失败。请始终使用 set -euo pipefail。
  2. 不要在发布路径中硬编码 .NET 版本。 使用 dotnet publish -o ./publish 明确指定输出目录。在构件路径中硬编码 net8.0,会导致升级到 .NET 9+ 时出错。
  3. 不要跳过预发布版本检测步骤。 包管理器提交(Homebrew、winget、Scoop、Chocolatey、NuGet)必须以稳定版本为前提。将 -rc.1 发布到 winget-pkgs 或 NuGet 并标记为稳定版会让用户困惑。
  4. 不要将 actions/upload-artifact v3 与 merge-multiple 搭配使用。 merge-multiple 参数需要 actions/download-artifact@v4。使用 v3 会静默忽略该标志,并创建嵌套目录。
  5. 不要忘记为中间构建构件设置 retention-days: 1。 发布构件会发布到 GitHub Releases(永久保存)。工作流构件是临时的,应尽快过期以节省存储空间。
  6. 不要在矩阵作业中使用 gh release create 创建 GitHub Releases。 只有发布作业(所有构建完成后)才应创建发布。矩阵作业负责上传构件;发布作业负责汇总它们。

参考资料

评论

支持 Markdown