Migrating CI/CD Pipelines from Azure DevOps to GitHub Actions: A Practical Guide
Assume you’re running builds, tests, and deployments on Azure DevOps, but your source of truth is already GitHub. Maintaining two disparate platforms increases operational overhead and introduces friction in your automation lifecycle. Centralizing CI/CD in GitHub Actions isn’t just cost-efficient—it’s often inevitable as repositories, code discussions, and workflow automation converge.
A linear migration rarely pans out. Duplicate pipelines, mismatched secrets, missing permissions, subtle YAML divergences: all can halt a move if overlooked. Below is a field-tested approach developed migrating both cloud-native and legacy .NET monoliths.
Platform Differences: Azure DevOps vs GitHub Actions
Table 1. Comparative Overview
Feature | Azure DevOps | GitHub Actions |
---|---|---|
YAML Pipelines | Supported, but syntax diverges | Supported, more Markdown-like |
Triggers | trigger , pr | on: with fine-grained filters |
Hosted Agents | windows , ubuntu , macOS | ubuntu-latest , etc. |
Secret Management | Service Connections & Vault | repo/org-level Secrets |
Marketplace Actions | Task-based, smaller ecosystem | Large community catalog |
Permission Model | RBAC, finer-grained | Simpler GH repo permissions |
Note: If you rely on Azure DevOps classic UI pipelines, migration requires a complete translation, not just a YAML lift-and-shift.
Step 1. Build a Pipeline Inventory
Skip this and you risk missing deployment triggers or test coverage. List:
- Pipeline names and locations
- Pipeline triggers (e.g., PR validation, nightly builds)
- Build/test environments (
windows-latest
,ubuntu-20.04
, or self-hosted runners?) - Integration points (Azure Key Vault, custom agents, artifact feeds)
- All secrets and service principals in-use
- State transitions (release approvals, manual interventions)
- Non-obvious: Any use of pipeline variables set at runtime
Pipeline | Trigger | Artifact | Environment | Notes |
---|---|---|---|---|
build-dotnet | PR, push | .NET DLL | ubuntu-latest | Custom NuGet source |
deploy-staging | Manual, tag | zip package | Azure App Service | Needs Azure connection string |
A spreadsheet isn’t overkill here. Gaps here equal outages later.
Step 2. Prepare GitHub Repo and Enable Actions
- Repository on GitHub (
main
branch ideally protected). - Actions enabled (default: yes, but confirm under Settings > Actions).
- Restrict workflow permissions (
Read/write
triggers security reviews; consider usingRead
and explicitly elevate in workflow when needed). - Map existing branch protection rules to GitHub. Subtle mismatch in required status checks or reviewers often blocks merges unexpectedly.
Step 3. Map Pipeline Triggers
Azure DevOps may use trigger:
and pr:
keys; GitHub expects an on:
block. Example translation:
Azure DevOps YAML:
trigger:
branches:
include:
- main
- release/*
pr:
branches:
include:
- main
GitHub Actions YAML:
on:
push:
branches:
- main
- 'release/**'
pull_request:
branches:
- main
Gotcha: Patterns differ. In GitHub Actions, 'release/*'
matches only one folder deep; 'release/**'
necessary for all descendants.
Step 4. Migrate Build & Test Logic
Task translations rarely map 1:1. For .NET projects:
Azure DevOps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '6.x'
- script: dotnet build --configuration Release
GitHub Actions:
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
- run: dotnet restore
- run: dotnet build --configuration Release --no-restore
- run: dotnet test --no-build --verbosity normal
- Note:
actions/setup-dotnet@v3
replacesUseDotNet
. Some environment variables (DOTNET_CLI_TELEMETRY_OPTOUT=1
) may now need to be set explicitly. - NuGet authentication to private feeds: In DevOps, handled via Service Connections; in Actions, add a secret then inject it in a step:
- name: Authenticate NuGet run: dotnet nuget add source --username ... --password ${{ secrets.NUGET_TOKEN }}
Step 5. Translate Deployment Steps
App Service deploys most often trip up migrations.
Azure DevOps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'my-azure-svc'
appName: 'prod-api'
package: '$(System.DefaultWorkingDirectory)/drop/*.zip'
GitHub Actions:
- uses: azure/webapps-deploy@v2
with:
app-name: prod-api
publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
package: './drop/*.zip'
- Obtain publish profile via Azure Portal > App Service > Get publish profile, store as
AZURE_PUBLISH_PROFILE
in repo secrets.
Known issue: Publish profile permission mismatches can hard-fail deploys with:
##[error]Failed to deploy web package to App Service.
If this occurs, verify publish profile scope and expiration on Azure.
Step 6. Migrate Secrets and Secure Connections
Do not port plaintext secrets. Instead:
Settings > Secrets and variables > Actions > New repository secret
- Store: AZURE_PUBLISH_PROFILE, any API keys, NuGet tokens, Slack webhooks, etc.
- Replace
$(Variable)
in Azure DevOps with${{ secrets.VARIABLE }}
in GitHub Actions.
Secret Name | Used For |
---|---|
AZURE_PUBLISH_PROFILE | Deployment auth |
NUGET_TOKEN | Private package feed |
SLACK_WEBHOOK_URL | Alert notifications |
Step 7. Progressive Workflow Validation
Blind “big-bang” cutovers break production. Safer path:
- Fork a feature branch, commit minimum viable workflow (
.github/workflows/ci.yml
). - Use manual triggers (
workflow_dispatch
) to verify build/test before PR triggers. - Observe real logs under the Actions tab—errors such as:
Usually mean path mismatches or missing secrets, not genuine code failures.##[error]The process '/usr/bin/dotnet' failed with exit code 1
- Validate artifact outputs. In Azure DevOps, artifacts are explicit; in GitHub Actions, use the
actions/upload-artifact
andactions/download-artifact
steps. - Once validated, restrict Azure DevOps triggers to avoid double deployments during the interim period.
Additional Recommendations
- For local workflow debugging,
nektos/act
provides limited but rapid feedback. Not all marketplace actions are fully supported—expect partial fidelity. - Modularize your workflow files—prefer composable jobs and use
needs
dependencies to isolate failures. - Use matrix strategies where parallel multi-version or OS testing is required (e.g., test across
ubuntu-latest
,windows-latest
). - Artifact expiration defaults differ. GitHub Actions deletes artifacts after 90 days; adjust via retention policy if necessary.
Example: Minimal .NET CI/CD Workflow
name: build-and-deploy
on:
push:
branches:
- main
jobs:
build-test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- run: dotnet restore
- run: dotnet build --configuration Release --no-restore
- run: dotnet test --no-build --verbosity normal
- uses: actions/upload-artifact@v3
with:
name: build_output
path: '**/*.zip'
deploy:
needs: build-test
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: build_output
- name: Deploy to Azure App Service
uses: azure/webapps-deploy@v2
with:
app-name: prod-api
publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
package: '**/*.zip'
Side Note: Artifact path globbing sometimes varies versus Azure DevOps; use exact patterns or debugging ls
steps to confirm.
Final Thoughts
Successful migration from Azure DevOps to GitHub Actions depends on rigorous pipeline inventory, careful YAML translation, and phased adoption with live testing. Pay special attention to the security context—secrets and permissions—since failure modes are silent until a deployment breaks.
There are alternative tools (e.g., scripting a conversion), but in practice, hand-crafting and incrementally testing each workflow yields fewer surprises and more maintainable builds. For organizations with both monorepos and polyrepos, consider the implications on pipeline scalability and concurrency.
If your migration fails silently or deploys to the wrong app, check path casing: Azure DevOps is case-insensitive with artifacts, GitHub runners using Ubuntu are not.
Questions or migration lessons? Share specifics—edge cases matter.