Skip to main content

Migrating from Azure DevOps to GitHub Actions: A Step-by-Step Guide

February 14, 2026 3 min read

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

  1. Migrate incrementally — start with the simplest pipeline, learn patterns, then tackle complex ones.
  2. Use OIDC — federated credentials are more secure and eliminate credential rotation.
  3. Invest in reusable workflows early — pays for itself by the third pipeline.
  4. Test on a branch first — get the workflow green before merging.
  5. Keep Azure DevOps as fallback — disable but don't delete for 30 days.
Share this post

Comments

Ajit Gangurde

Software Engineer II at Microsoft | 15+ years in .NET & Azure