Migrating from Azure DevOps to GitHub Actions: A Step-by-Step Guide
Migrating CI/CD from Azure DevOps to GitHub Actions is where teams spend most of their effort — not because Actions is harder, but because Azure DevOps pipelines accumulate years of tribal knowledge. Here's the practical translation guide with real YAML examples and a migration checklist to keep things on track.
Conceptual Mapping
Azure DevOps → GitHub Actions
──────────────────────────────────────────────
Pipeline → Workflow
Stage → Job (with needs: for ordering)
Step/Task → Step/Action
Variable Group → Environment secrets / Variables
Service Connection → OIDC / Federated credentials
Agent Pool → Runner (hosted or self-hosted)
Environment + Approvals → Environment + Protection rules
Template → Reusable workflow / Composite action
The biggest shift: Azure DevOps's Pipeline → Stage → Job → Step becomes Workflow → Job → Step. Stages become separate jobs with needs: dependencies. Once this mental model clicks, the rest falls into place.
YAML Translation Example
Here's a .NET build-and-deploy pipeline translated to GitHub Actions:
name: Build and Deploy
on:
push:
branches: [main, 'release/*']
pull_request:
branches: [main]
env:
BUILD_CONFIGURATION: Release
DOTNET_VERSION: '9.0.x'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- run: dotnet restore
- run: dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore
- run: dotnet test --configuration ${{ env.BUILD_CONFIGURATION }} --no-build
- run: dotnet publish --configuration ${{ env.BUILD_CONFIGURATION }} --output ./publish
- uses: actions/upload-artifact@v4
with:
name: webapp
path: ./publish
deploy-staging:
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: staging
permissions:
id-token: write
contents: read
steps:
- uses: actions/download-artifact@v4
with:
name: webapp
path: ./publish
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- uses: azure/webapps-deploy@v3
with:
app-name: myapp-staging
package: ./publish
Key differences: checkout is explicit (actions/checkout@v4), OIDC replaces service connections, and stages become jobs with needs:. Also note the permissions block — GitHub Actions uses a least-privilege model where you explicitly declare what the workflow needs access to.
Reusable Workflows: The Template Equivalent
In Azure DevOps, you'd use pipeline templates to share logic. In GitHub Actions, reusable workflows serve the same purpose. Define a callable workflow once and invoke it from multiple pipelines:
# .github/workflows/dotnet-build-reusable.yml
name: .NET Build (Reusable)
on:
workflow_call:
inputs:
dotnet-version:
required: false
type: string
default: '9.0.x'
project-path:
required: true
type: string
outputs:
artifact-name:
value: ${{ jobs.build.outputs.artifact-name }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-name: ${{ steps.set-name.outputs.name }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ inputs.dotnet-version }}
- run: dotnet restore ${{ inputs.project-path }}
- run: dotnet build ${{ inputs.project-path }} -c Release --no-restore
- run: dotnet test ${{ inputs.project-path }} -c Release --no-build
- id: set-name
run: echo "name=build-$(date +%s)" >> $GITHUB_OUTPUT
Calling workflows consume this with a simple uses reference, passing only the inputs that differ between projects. This eliminates duplication across repositories and ensures consistency in build processes.
Secrets and Variables
Map Variable Groups to GitHub's three-tier secret model:
- Repository secrets — shared across environments (e.g.,
AZURE_TENANT_ID) - Environment secrets — environment-specific (e.g.,
DB_CONNECTION) - Organization secrets — shared across repos (e.g., registry credentials)
For Secure Files, base64-encode the file and store as a secret:
- run: echo "${{ secrets.CERT_PFX }}" | base64 --decode > cert.pfx
Setting Up OIDC for Azure
OIDC (OpenID Connect) federated credentials replace service connections. This eliminates stored secrets entirely — the workflow requests a short-lived token from Azure AD using the GitHub OIDC provider:
# Create federated credential for your GitHub repo
az ad app federated-credential create \
--id <APP_OBJECT_ID> \
--parameters '{
"name": "github-main-branch",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:your-org/your-repo:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
Then in your workflow, the azure/login@v2 action handles the token exchange automatically. No more rotating service principal secrets every 90 days.
Migration Checklist
Pre-Migration: Document variable groups, list service connections, inventory pipeline tasks, map environments and approval gates. Export your existing YAML pipelines for reference.
Repository Setup: Configure branch protection rules, set up environments with protection rules, configure OIDC federation for Azure deployments.
Validation: Run workflow on a feature branch, verify tests/artifacts/deployments, compare build times, run 5+ builds to check for flakiness.
Cutover: Disable (don't delete) Azure DevOps pipelines, monitor first production deployment, keep as fallback for 30 days.
Common Gotchas
- Matrix builds replace Azure DevOps strategy patterns — use
strategy.matrixfor multi-platform or multi-version builds - Caching needs explicit setup with
actions/cache@v4— Azure DevOps caches NuGet/npm automatically - Artifact retention defaults to 90 days in GitHub Actions vs. configurable in Azure DevOps — set
retention-daysexplicitly - Self-hosted runners require different setup than Azure DevOps agents — use runner groups for organization-level management
Key Takeaways
- Migrate incrementally — start with the simplest pipeline, learn patterns, then tackle complex ones.
- Use OIDC — federated credentials are more secure and eliminate credential rotation.
- Invest in reusable workflows early — pays for itself by the third pipeline.
- Test on a branch first — get the workflow green before merging.
- Keep Azure DevOps as fallback — disable but don't delete for 30 days.
References
- Migrating from Azure Pipelines to GitHub Actions — Official GitHub migration guide with syntax mapping
- GitHub Actions documentation — Comprehensive guide to workflows, actions, and runners
- Configuring OpenID Connect in Azure — Setting up OIDC federated credentials for Azure deployments
- Reusable workflows — How to create and call reusable workflows across repositories
- Azure Login GitHub Action — Official Azure login action for GitHub Actions with OIDC support
Comments
Ajit Gangurde
Software Engineer II at Microsoft | 15+ years in .NET & Azure