dotnet-gha-deploy
dotnet-gha-deploy
Deployment patterns for .NET applications in GitHub Actions: GitHub Pages deployment for documentation sites (Starlight/Docusaurus), container registry push patterns for GHCR and ACR, Azure Web Apps deployment via azure/webapps-deploy, GitHub Environments with protection rules for staged rollouts, and rollback strategies for failed deployments.
Version assumptions: GitHub Actions workflow syntax v2. azure/webapps-deploy@v3 for Azure App Service. azure/login@v2 for Azure credential management. GitHub Environments for deployment gates.
Scope
- Azure Web Apps deployment via azure/webapps-deploy
- GitHub Pages deployment for documentation sites
- Container registry push patterns for GHCR and ACR
- GitHub Environments with protection rules
- Rollback strategies for failed deployments
Out of scope
- Container orchestration (Kubernetes, Docker Compose) -- see [skill:dotnet-container-deployment]
- Container image authoring -- see [skill:dotnet-containers]
- NuGet publishing and container builds -- see [skill:dotnet-gha-publish]
- Starter CI templates -- see [skill:dotnet-add-ci]
- Azure DevOps deployment -- see [skill:dotnet-ado-unique] and [skill:dotnet-ado-publish]
- CLI release pipelines -- see [skill:dotnet-cli-release-pipeline]
Cross-references: [skill:dotnet-container-deployment] for container orchestration patterns, [skill:dotnet-containers] for container image authoring, [skill:dotnet-add-ci] for starter CI templates, [skill:dotnet-cli-release-pipeline] for CLI-specific release automation.
GitHub Pages Deployment for Documentation
Static Site Deployment (Starlight/Docusaurus)
Deploy a .NET project's documentation site to 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
Key decisions:
concurrency.cancel-in-progress: falseprevents cancelling an in-progress Pages deploymentid-token: writepermission is required for the Pages deployment token- Separate
buildanddeployjobs allow the deploy job to use thegithub-pagesenvironment with protection rules
API Documentation from XML Comments
Generate and deploy API reference documentation from .NET XML comments:
- 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
Container Registry Push Patterns
Push to GHCR with Environment Gates
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"
Promote by Digest (Immutable Deployments)
Use image digest references for immutable deployments across environments:
- 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
Digest-based promotion ensures the exact same image bytes are deployed to production, regardless of tag mutations.
Azure Web Apps Deployment
Deploy via 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 with Deployment Slots
Use deployment slots for zero-downtime deployments with pre-swap validation:
- 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 Authentication (Federated Credentials)
Use OIDC for passwordless Azure authentication instead of service principal secrets:
- 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 requires configuring a federated credential in Azure AD that trusts the GitHub Actions OIDC provider. No client secret is stored in GitHub Secrets.
GitHub Environments with Protection Rules
Multi-Environment Pipeline
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"
Protection Rule Configuration
Configure in GitHub Settings > Environments for each environment:
| Environment | Required Reviewers | Wait Timer | Branch Policy |
|---|---|---|---|
| development | None | None | Any branch |
| staging | 1 reviewer | None | main, release/* |
| production | 2 reviewers | 15 minutes | main only |
Environment-Specific Secrets and Variables
Each environment can override repository-level secrets:
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"
Rollback Patterns
Revert Deployment
Re-deploy the previous known-good version on failure:
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 Deployment Slot Rollback
Swap back to the previous slot on health check failure:
- 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"
Manual Rollback via workflow_dispatch
Provide a manual trigger for emergency rollbacks:
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 Gotchas
- Use
set -euo pipefailin all multi-line bash steps -- withoutpipefail, failures in piped commands are silently swallowed, producing false-green deployments. - Never use
cancel-in-progress: truefor deployment concurrency groups -- cancelling an in-progress deployment can leave infrastructure in a partially deployed state. - Always run health checks after deployment -- a successful
deploystep does not guarantee the application is running correctly; verify with HTTP health checks. - Use
id-token: writepermission for OIDC Azure login -- without it, the federated credential exchange fails with a cryptic 403 error. - Deployment slot swaps are atomic -- if the swap fails, both slots retain their original deployments; no partial state.
- Never hardcode Azure credentials in workflow files -- use OIDC federated credentials or environment-scoped secrets; hardcoded secrets in YAML are visible in repository history.
- Use digest-based image references for production deployments -- tags are mutable and can be overwritten; digests are immutable and guarantee the exact image bytes.
- Separate build and deploy jobs -- build artifacts once, deploy to multiple environments from the same artifact to ensure consistency.
More from novotnyllc/dotnet-artisan
dotnet-csharp
Baseline C# skill loaded for every .NET code path. Guides language patterns (records, pattern matching, primary constructors, C# 8-15), coding standards, async/await, DI, LINQ, serialization, domain modeling, concurrency, Roslyn analyzers, globalization, native interop (P/Invoke, LibraryImport, ComWrappers), WASM interop (JSImport/JSExport), and type design. Spans 25 topics. Do not use for ASP.NET endpoint architecture, UI framework patterns, or CI/CD guidance.
129dotnet-ui
Builds .NET UI apps across Blazor (Server, WASM, Hybrid, Auto), MAUI (XAML, MVVM, Shell, Native AOT), Uno Platform (MVUX, Extensions, Toolkit), WPF (.NET 8+, Fluent theme), WinUI 3 (Windows App SDK, MSIX, Mica/Acrylic, adaptive layout), and WinForms (high-DPI, dark mode) with JS interop, accessibility (SemanticProperties, ARIA), localization (.resx, RTL), platform bindings (Java.Interop, ObjCRuntime), and framework selection. Spans 20 topic areas. Do not use for backend API design or CI/CD pipelines.
99dotnet-api
Builds ASP.NET Core APIs, EF Core data access, gRPC, SignalR, and backend services with middleware, security (OAuth, JWT, OWASP), resilience, messaging, OpenAPI, .NET Aspire, Semantic Kernel, HybridCache, YARP reverse proxy, output caching, Office documents (Excel, Word, PowerPoint), PDF, and architecture patterns. Spans 32 topic areas. Do not use for UI rendering patterns or CI/CD pipeline authoring.
90dotnet-testing
Defines .NET test strategy and implementation patterns across xUnit v3 (Facts, Theories, fixtures, IAsyncLifetime), integration testing (WebApplicationFactory, Testcontainers), Aspire testing (DistributedApplicationTestingBuilder), snapshot testing (Verify, scrubbing), Playwright E2E browser automation, BenchmarkDotNet microbenchmarks, code coverage (Coverlet), mutation testing (Stryker.NET), UI testing (page objects, selectors), and AOT WASM test compilation. Spans 13 topic areas. Do not use for production API architecture or CI workflow authoring.
86dotnet-advisor
Routes .NET/C# requests to the correct domain skill and loads coding standards as baseline for all code paths. Determines whether the task needs API, UI, testing, devops, tooling, or debugging guidance based on prompt analysis and project signals, then invokes skills in the right order. Always invoked after [skill:using-dotnet] detects .NET intent. Do not use for deep API, UI, testing, devops, tooling, or debugging implementation guidance.
60dotnet-debugging
Debugs Windows and Linux/macOS applications (native, .NET/CLR, mixed-mode) with WinDbg MCP (crash dumps, !analyze, !syncblk, !dlk, !runaway, !dumpheap, !gcroot, BSOD), dotnet-dump, lldb with SOS, createdump, and container diagnostics (Docker, Kubernetes). Hang/deadlock diagnosis, high CPU triage, memory leak investigation, kernel debugging, and dotnet-monitor for production. Spans 17 topic areas. Do not use for routine .NET SDK profiling, benchmark design, or CI test debugging.
57