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.
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.
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:.
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
Migration Checklist
Pre-Migration: Document variable groups, list service connections, inventory pipeline tasks, map environments and approval gates.
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.
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.
Comments
Ajit Gangurde
Software Engineer II at Microsoft | 15+ years in .NET & Azure