.agents/skills/dotnet-gha-patterns/SKILL.md
Composes GitHub Actions workflows. Reusable workflows, composite actions, matrix, caching.
npx skillsauth add dodyg/blue-nile-pds dotnet-gha-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Composable GitHub Actions workflow patterns for .NET projects: reusable workflows with workflow_call, composite actions for shared step sequences, matrix builds across TFMs and operating systems, path-based triggers, concurrency groups for duplicate run cancellation, environment protection rules, NuGet and SDK caching strategies, and workflow_dispatch inputs for manual triggers.
Version assumptions: GitHub Actions workflow syntax v2. actions/setup-dotnet@v4 for .NET 8/9/10 support. actions/cache@v4 for dependency caching.
Cross-references: [skill:dotnet-add-ci] for starter templates that these patterns extend, [skill:dotnet-cli-release-pipeline] for CLI-specific release automation, [skill:dotnet-ci-benchmarking] for benchmark-specific CI integration.
workflow_call)Reusable workflows allow callers to invoke an entire workflow as a single step. Define inputs, outputs, and secrets for a clean contract:
# .github/workflows/build-reusable.yml
name: Build (Reusable)
on:
workflow_call:
inputs:
dotnet-version:
description: '.NET SDK version to install'
required: false
type: string
default: '8.0.x'
configuration:
description: 'Build configuration'
required: false
type: string
default: 'Release'
project-path:
description: 'Path to solution or project file'
required: true
type: string
outputs:
artifact-name:
description: 'Name of the uploaded build artifact'
value: ${{ jobs.build.outputs.artifact-name }}
secrets:
NUGET_AUTH_TOKEN:
description: 'NuGet feed authentication token'
required: false
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-name: build-${{ github.sha }}
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ inputs.dotnet-version }}
- name: Restore
run: dotnet restore ${{ inputs.project-path }}
- name: Build
run: dotnet build ${{ inputs.project-path }} -c ${{ inputs.configuration }} --no-restore
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: build-${{ github.sha }}
path: |
**/bin/${{ inputs.configuration }}/**
retention-days: 7
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
uses: ./.github/workflows/build-reusable.yml
with:
dotnet-version: '8.0.x'
project-path: MyApp.sln
secrets:
NUGET_AUTH_TOKEN: ${{ secrets.NUGET_AUTH_TOKEN }}
test:
needs: build
uses: ./.github/workflows/test-reusable.yml
with:
dotnet-version: '8.0.x'
project-path: MyApp.sln
Reference workflows from other repositories using the full path:
jobs:
build:
uses: my-org/.github-workflows/.github/workflows/dotnet-build.yml@v1
with:
dotnet-version: '9.0.x'
secrets: inherit # pass all secrets from caller
Use secrets: inherit when the reusable workflow needs access to the same secrets as the calling workflow without explicit enumeration.
Composite actions bundle multiple steps into a single reusable action. Use them for shared step sequences that appear across multiple workflows:
# .github/actions/dotnet-setup/action.yml
name: 'Setup .NET Environment'
description: 'Install .NET SDK and restore NuGet packages with caching'
inputs:
dotnet-version:
description: '.NET SDK version'
required: false
default: '8.0.x'
project-path:
description: 'Path to solution or project'
required: true
runs:
using: 'composite'
steps:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ inputs.dotnet-version }}
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }}
restore-keys: |
nuget-${{ runner.os }}-
- name: Restore dependencies
shell: bash
run: dotnet restore ${{ inputs.project-path }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET environment
uses: ./.github/actions/dotnet-setup
with:
dotnet-version: '9.0.x'
project-path: MyApp.sln
- name: Build
run: dotnet build MyApp.sln -c Release --no-restore
| Feature | Reusable Workflow | Composite Action |
|---------|------------------|-----------------|
| Scope | Entire job with runner | Steps within a job |
| Runner selection | Own runs-on | Caller's runner |
| Secrets access | Explicit or inherit | Caller's context |
| Outputs | Job-level outputs | Step-level outputs |
| Best for | Complete build/test/deploy jobs | Shared setup/teardown sequences |
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
dotnet-version: ['8.0.x', '9.0.x']
include:
- os: ubuntu-latest
dotnet-version: '10.0.x'
exclude:
- os: macos-latest
dotnet-version: '8.0.x'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup .NET ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ matrix.dotnet-version }}
- name: Test
run: dotnet test --framework net${{ matrix.dotnet-version == '8.0.x' && '8.0' || matrix.dotnet-version == '9.0.x' && '9.0' || '10.0' }}
Key decisions:
fail-fast: false ensures all matrix combinations run even if one fails, giving full signal on which platforms/TFMs are brokeninclude adds specific combinations not in the Cartesian productexclude removes combinations that are unnecessary or unsupportedGenerate matrix values dynamically for complex scenarios:
jobs:
compute-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- id: set-matrix
shell: bash
run: |
set -euo pipefail
# Extract TFMs from Directory.Build.props or csproj files
TFMS=$(grep -rh '<TargetFrameworks\?>' **/*.csproj | \
sed 's/.*<TargetFrameworks\?>//' | sed 's/<.*//' | \
tr ';' '\n' | sort -u | jq -R . | jq -sc .)
echo "matrix={\"tfm\":$TFMS}" >> "$GITHUB_OUTPUT"
test:
needs: compute-matrix
strategy:
matrix: ${{ fromJson(needs.compute-matrix.outputs.matrix) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: dotnet test --framework ${{ matrix.tfm }}
Trigger workflows only when relevant files change. Reduces CI cost and feedback time:
on:
push:
branches: [main]
paths:
- 'src/**'
- 'tests/**'
- '*.sln'
- 'Directory.Build.props'
- 'Directory.Packages.props'
- '.github/workflows/ci.yml'
pull_request:
branches: [main]
paths:
- 'src/**'
- 'tests/**'
- '*.sln'
- 'Directory.Build.props'
- 'Directory.Packages.props'
Use paths-ignore to skip builds for documentation-only changes:
on:
push:
branches: [main]
paths-ignore:
- 'docs/**'
- '*.md'
- 'LICENSE'
- '.editorconfig'
Choose paths or paths-ignore, not both. When both are specified on the same event, paths-ignore is ignored. Use paths (allowlist) for focused workflows; use paths-ignore (denylist) for broad workflows.
Prevent wasted CI time by cancelling in-progress runs when new commits are pushed to the same branch or PR:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Prevent parallel deployments to the same environment:
concurrency:
group: deploy-production
cancel-in-progress: false # queue, do not cancel deployments
Use cancel-in-progress: true for build/test (newer commit supersedes older), but cancel-in-progress: false for deployments (do not cancel an in-progress deploy).
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- name: Deploy to staging
run: echo "Deploying..."
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- name: Deploy to production
run: echo "Deploying..."
Configure protection rules in GitHub Settings > Environments:
| Rule | Purpose |
|------|---------|
| Required reviewers | Manual approval before deployment |
| Wait timer | Cooldown period (e.g., 15 minutes) |
| Branch restrictions | Only main or release/* branches can deploy |
| Custom deployment protection rules | Third-party integrations (monitoring checks) |
Environments can have their own secrets that override repository-level secrets. Use environment-scoped secrets for deployment credentials:
jobs:
deploy:
environment: production
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
# These resolve to environment-specific values
CONNECTION_STRING: ${{ secrets.CONNECTION_STRING }}
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.sh
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }}
restore-keys: |
nuget-${{ runner.os }}-
The restore-keys prefix match ensures a partial cache hit when csproj files change (most packages remain cached).
For self-hosted runners or scenarios where SDK installation is slow:
- name: Setup .NET with cache
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
cache: true
cache-dependency-path: '**/packages.lock.json'
The cache: true option in actions/setup-dotnet@v4 enables built-in NuGet caching using packages.lock.json as the cache key.
.NET 9 introduced MSBuild build-check caching. For incremental CI builds:
- name: Cache build output
uses: actions/cache@v4
with:
path: |
**/bin/
**/obj/
key: build-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/*.cs') }}
restore-keys: |
build-${{ runner.os }}-
Use build output caching cautiously -- stale caches can mask build errors. Prefer NuGet caching as the primary CI speed optimization.
workflow_dispatch Inputson:
workflow_dispatch:
inputs:
environment:
description: 'Target deployment environment'
required: true
type: choice
options:
- staging
- production
default: staging
version:
description: 'Version to deploy (e.g., 1.2.3)'
required: true
type: string
dry-run:
description: 'Simulate deployment without applying changes'
required: false
type: boolean
default: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
with:
ref: v${{ inputs.version }}
- name: Deploy
env:
DRY_RUN: ${{ inputs.dry-run }}
run: |
set -euo pipefail
if [ "$DRY_RUN" = "true" ]; then
echo "DRY RUN: would deploy v${{ inputs.version }} to ${{ inputs.environment }}"
else
./deploy.sh --version ${{ inputs.version }}
fi
Input types: string, boolean, choice, environment (selects from configured environments).
paths and paths-ignore on the same event -- when both are specified, paths-ignore is silently ignored. Use one or the other.fail-fast: false on matrix builds -- default fail-fast: true cancels sibling jobs when one fails, hiding which other combinations also break.set -euo pipefail in all bash steps -- without pipefail, a non-zero exit from a piped command (e.g., script | tee) does not fail the step.type: in the workflow_call inputs.runner.os -- NuGet packages are OS-dependent; a Linux-built cache restoring on Windows causes restore failures.secrets: inherit passes all caller secrets -- use explicit secret declarations for security-sensitive reusable workflows to limit exposure.cancel-in-progress: false -- cancelling an in-progress deployment can leave infrastructure in an inconsistent state.testing
Get best practices for TUnit unit testing, including data-driven tests
development
Severity scoring, scorecard computation, confidence levels, and remediation tracking for web accessibility audits. Use when computing page accessibility scores (0-100 with A-F grades), tracking remediation progress across audits, or generating cross-page comparison scorecards.
development
Web content discovery, URL crawling, and page inventory for accessibility audits. Use when scanning web pages, crawling sites for audit scope, or building page inventories for multi-page audits.
development
Audit report formatting, severity scoring, scorecard computation, and compliance export for document accessibility audits. Use when generating DOCUMENT-ACCESSIBILITY-AUDIT.md reports, computing document severity scores (0-100 with A-F grades), creating VPAT/ACR compliance exports, or formatting remediation priorities.