关于 dotnet-gha-deploy
为 .NET 项目提供 GitHub Actions 部署模式,面向维护文档站点、容器和 Azure Web Apps 部署流水线的开发者。内容涵盖环境审批、OIDC 身份验证和回滚。
主要功能
- 01GitHub Pages 文档部署
- 02GHCR 与 ACR 镜像推送
- 03Azure Web Apps 部署
- 04GitHub Environments 部署门禁
- 05部署回滚策略
适用场景
- 01将项目文档发布到 GitHub Pages
- 02按环境推广相同容器镜像
- 03通过部署槽发布 Azure Web Apps
- 04部署失败后恢复先前版本
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-gha-deploy安装后在 Claude Code 中输入/dotnet-gha-deploy确认已加载。
详细介绍
dotnet-gha-deploy
GitHub Actions 中 .NET 应用程序的部署模式:文档站点(Starlight/Docusaurus)的 GitHub Pages 部署、GHCR 和 ACR 的容器注册表推送模式、通过 azure/webapps-deploy 部署到 Azure Web Apps、使用保护规则实现分阶段发布的 GitHub Environments,以及部署失败后的回滚策略。
版本假设: GitHub Actions 工作流语法 v2。Azure App Service 使用 azure/webapps-deploy@v3。使用 azure/login@v2 管理 Azure 凭据。使用 GitHub Environments 设置部署门禁。
范围边界: 此技能负责 GitHub Actions 部署流水线模式。容器编排和运行时配置由 [skill:dotnet-container-deployment] 负责。容器映像编写由 [skill:dotnet-containers] 负责。发布(NuGet 推送、容器构建)归属 [skill:dotnet-gha-publish]。可组合工作流模式见 [skill:dotnet-gha-patterns]。入门 CI 模板由 [skill:dotnet-add-ci] 负责。
范围之外: 容器编排(Kubernetes、Docker Compose)——参见 [skill:dotnet-container-deployment]。容器映像编写——参见 [skill:dotnet-containers]。NuGet 发布和容器构建——参见 [skill:dotnet-gha-publish]。入门 CI 模板——参见 [skill:dotnet-add-ci]。Azure DevOps 部署——参见 [skill:dotnet-ado-patterns]。CLI 发布流水线——参见 [skill:dotnet-cli-release-pipeline]。
交叉引用:[skill:dotnet-container-deployment](容器编排模式)、[skill:dotnet-containers](容器映像编写)、[skill:dotnet-add-ci](入门 CI 模板)、[skill:dotnet-cli-release-pipeline](CLI 专用发布自动化)。
GitHub Pages 文档部署
静态站点部署(Starlight/Docusaurus)
将 .NET 项目的文档站点部署到 GitHub Pages:
name: Deploy Docs
on:
push:
branches: [main]
paths:
- 'docs/**'
- '.github/workflows/deploy-docs.yml'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: docs/package-lock.json
- name: Install dependencies
working-directory: docs
run: npm ci
- name: Build documentation site
working-directory: docs
run: npm run build
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
关键决策:
concurrency.cancel-in-progress: false可防止取消正在进行的 Pages 部署- Pages 部署令牌需要
id-token: write权限 - 将
build和deploy作业分开,可让 deploy 作业使用带有保护规则的github-pages环境
从 XML 注释生成 API 文档
根据 .NET XML 注释生成并部署 API 参考文档:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build with XML docs
run: |
set -euo pipefail
dotnet build src/MyLibrary/MyLibrary.csproj \
-c Release \
-p:GenerateDocumentationFile=true
- name: Generate API docs with docfx
run: |
set -euo pipefail
dotnet tool install -g docfx
docfx docs/docfx.json
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/_site
容器注册表推送模式
通过环境门禁推送到 GHCR
jobs:
build:
runs-on: ubuntu-latest
outputs:
image-digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- name: Deploy container to staging
run: |
set -euo pipefail
echo "Deploying ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }} to staging"
# Platform-specific deployment command here
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- name: Deploy container to production
run: |
set -euo pipefail
echo "Deploying ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }} to production"
按摘要提升(不可变部署)
使用映像摘要引用,在各环境之间进行不可变部署:
- name: Retag for production
run: |
set -euo pipefail
# Pull by digest (immutable), retag for production
docker pull ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }}
docker tag ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }} \
ghcr.io/${{ github.repository }}:production
docker push ghcr.io/${{ github.repository }}:production
基于摘要的提升可确保部署到生产环境的是完全相同的映像字节,不受标签变更影响。
Azure Web Apps 部署
通过 azure/webapps-deploy 部署
name: Deploy to Azure
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Publish
run: |
set -euo pipefail
dotnet publish src/MyApp/MyApp.csproj \
-c Release \
-o ./publish
- name: Upload publish artifact
uses: actions/upload-artifact@v4
with:
name: webapp
path: ./publish
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment:
name: staging
url: https://myapp-staging.azurewebsites.net
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: webapp
path: ./publish
- name: Login to Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: myapp-staging
package: ./publish
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.azurewebsites.net
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: webapp
path: ./publish
- name: Login to Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: myapp-production
package: ./publish
带部署槽位的 Azure Web App
使用部署槽位实现零停机部署,并在交换前进行验证:
- name: Deploy to staging slot
uses: azure/webapps-deploy@v3
with:
app-name: myapp-production
slot-name: staging
package: ./publish
- name: Validate staging slot
shell: bash
run: |
set -euo pipefail
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
https://myapp-production-staging.azurewebsites.net/healthz)
if [ "$HTTP_STATUS" != "200" ]; then
echo "Health check failed with status $HTTP_STATUS"
exit 1
fi
- name: Swap slots
uses: azure/cli@v2
with:
inlineScript: |
az webapp deployment slot swap \
--resource-group myapp-rg \
--name myapp-production \
--slot staging \
--target-slot production
OIDC 身份验证(联合凭据)
使用 OIDC 进行无密码 Azure 身份验证,而不是使用服务主体密钥:
- name: Login to Azure (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
OIDC 要求在 Azure AD 中配置联合凭据,以信任 GitHub Actions OIDC 提供方。GitHub Secrets 中不会存储客户端密钥。
带保护规则的 GitHub Environments
多环境流水线
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: dotnet publish -c Release -o ./publish
- uses: actions/upload-artifact@v4
with:
name: app
path: ./publish
deploy-dev:
needs: build
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/download-artifact@v4
with:
name: app
- run: echo "Deploy to dev"
deploy-staging:
needs: deploy-dev
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/download-artifact@v4
with:
name: app
- run: echo "Deploy to staging"
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/download-artifact@v4
with:
name: app
- run: echo "Deploy to production"
保护规则配置
在每个环境的 GitHub Settings > Environments 中进行配置:
| 环境 | 所需审核者 | 等待时间 | 分支策略 |
|---|---|---|---|
| development | 无 | 无 | 任意分支 |
| staging | 1 位审核者 | 无 | main、release/* |
| production | 2 位审核者 | 15 分钟 | 仅限 main |
特定环境的密钥和变量
每个环境都可以覆盖仓库级别的密钥:
jobs:
deploy:
environment: production
runs-on: ubuntu-latest
steps:
- name: Deploy with environment-specific config
env:
# Resolves to the production environment's secret, not the repo-level one
DB_CONNECTION: ${{ secrets.DB_CONNECTION_STRING }}
APP_URL: ${{ vars.APP_URL }}
run: |
set -euo pipefail
echo "Deploying to $APP_URL"
回滚模式
回退部署
发生故障时,重新部署上一个已知正常的版本:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy new version
id: deploy
continue-on-error: true
run: |
set -euo pipefail
# Deploy logic here
./deploy.sh --version ${{ github.sha }}
- name: Health check
id: health
if: steps.deploy.outcome == 'success'
continue-on-error: true
shell: bash
run: |
set -euo pipefail
for i in {1..5}; do
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://example.com/healthz)
if [ "$HTTP_STATUS" = "200" ]; then
echo "Health check passed"
exit 0
fi
sleep 10
done
echo "Health check failed after 5 attempts"
exit 1
- name: Rollback on failure
if: steps.deploy.outcome == 'failure' || steps.health.outcome == 'failure'
run: |
set -euo pipefail
echo "Rolling back to previous version"
# Re-deploy the last known-good artifact
./deploy.sh --version ${{ github.event.before }}
- name: Fail the job if rollback was needed
if: steps.deploy.outcome == 'failure' || steps.health.outcome == 'failure'
run: exit 1
Azure 部署槽位回滚
健康检查失败时,切换回之前的槽位:
- name: Swap to production
id: swap
uses: azure/cli@v2
with:
inlineScript: |
az webapp deployment slot swap \
--resource-group myapp-rg \
--name myapp-production \
--slot staging \
--target-slot production
- name: Post-swap health check
id: post-health
continue-on-error: true
shell: bash
run: |
set -euo pipefail
sleep 30 # allow swap to stabilize
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://myapp.azurewebsites.net/healthz)
if [ "$HTTP_STATUS" != "200" ]; then
echo "Post-swap health check failed"
exit 1
fi
- name: Rollback swap on failure
if: steps.post-health.outcome == 'failure'
uses: azure/cli@v2
with:
inlineScript: |
az webapp deployment slot swap \
--resource-group myapp-rg \
--name myapp-production \
--slot staging \
--target-slot production
echo "Rolled back: swapped staging back to production"
通过 workflow_dispatch 手动回滚
提供手动触发方式,以便在紧急情况下回滚:
on:
workflow_dispatch:
inputs:
version:
description: 'Version to roll back to (e.g., v1.2.3)'
required: true
type: string
environment:
description: 'Target environment'
required: true
type: choice
options:
- staging
- production
jobs:
rollback:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.version }}
- name: Publish
run: |
set -euo pipefail
dotnet publish src/MyApp/MyApp.csproj -c Release -o ./publish
- name: Deploy rollback version
run: |
set -euo pipefail
echo "Rolling back ${{ inputs.environment }} to ${{ inputs.version }}"
# Platform-specific deployment
Agent 注意事项
- 在所有多行 bash 步骤中使用
set -euo pipefail—— 不使用pipefail时,管道命令中的失败会被悄悄忽略,导致部署看似成功。 - 部署并发组中绝不要使用
cancel-in-progress: true—— 取消正在进行的部署可能导致基础设施处于部分部署状态。 - 部署后务必运行运行状况检查 ——
deploy步骤成功并不代表应用运行正常;请通过 HTTP 运行状况检查进行验证。 - 使用
id-token: write权限进行 OIDC Azure 登录 —— 缺少此权限时,联合凭据交换会失败,并返回含义不明的 403 错误。 - 部署槽位交换是原子操作 —— 如果交换失败,两个槽位都会保留原有部署,不会出现部分状态。
- 绝不要在工作流文件中硬编码 Azure 凭据 —— 请使用 OIDC 联合凭据或限定环境的机密;硬编码在 YAML 中的机密会在仓库历史记录中公开。
- 生产部署使用基于摘要的镜像引用 —— 标签可以修改和覆盖;摘要不可变,能够保证镜像字节完全一致。
- 将构建和部署拆分为不同的作业 —— 只构建一次构建产物,再从同一构建产物部署到多个环境,以确保一致性。