From Azure Pipelines to GitHub Actions

I use Azure Pipelines a lot, both for CI/CD (including automated testing for Azure Data Factory) and for building disposable Azure environments repeatably. Most of my code, however, lives in GitHub. GitHub provides its own service for automating CI/CD software workflows – GitHub Actions – and in this post I compare it to Azure Pipelines.

This might be useful background if you're considering migrating to GitHub Actions or just curious about the comparison. GitHub's guide to migrating from Azure Pipelines to GitHub Actions provides more detailed information, but doesn't say as much about missing features.

This is a short Azure Pipeline I built to give me something to reproduce in GitHub actions. It purposely doesn't do much – it's a simple example to enable comparison.

name: AzurePipelines-HelloWorld

trigger: [ main ]

pool:
  vmImage: ubuntu-latest

steps:

- pwsh: Write-Host "Hello from Azure Pipelines!"
  displayName: Say hello

- bash: ls -la $(System.DefaultWorkingDirectory)/.github/workflows
  displayName: Inspect GitHub workflows folder

- task: UseDotNet@2
  displayName: Use .NET Core
  inputs:
    version: 3.1.x

It's executed on commits to my main branch and does three things:

  • prints a greeting
  • lists some files
  • installs .NET Core on the build agent.

GitHub Actions are conceptually close to Azure Pipelines, but with differences in syntax. There's a table of corresponding concepts and keywords below. GitHub calls a pipeline a workflow – a workflow is defined in a YAML file, just like an Azure Pipeline1).

The first difference appears even before you write any code: in Azure, a pipeline is an object defined in Azure DevOps with an associated YAML file. In GitHub, you save a workflow YAML file in repo folder /.github/workflows and it is the pipeline – no further configuration required.

Here's a GitHub workflow equivalent to the Azure pipeline above:

name: GitHubActions-HelloWorld

on:
  push:
    branches: [ main ]

jobs:
  examplejob:  
    runs-on: ubuntu-latest

    steps:

    - name: Say hello
      run: Write-Host "Hello from GitHub Actions!"
      shell: pwsh
      
    - name: Checkout repo
      uses: actions/checkout@v2
      
    - name: Inspect GitHub workflows folder 
      run: ls -la $GITHUB_WORKSPACE/.github/workflows  
      shell: bash
      
    - name: Use .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.x

So what's different?

  • GitHub uses on to configure all execution triggers for a workflow, with event type arguments indicating which specific events should trigger a workflow. Azure uses a set of distinct keywords for this:

    • trigger (used in my example) is equivalent to on push
    • pr is equivalent to on pull_request
    • schedules is equivalent to on schedule.

    Unlike Azure Pipelines, GitHub workflows can't be triggered manually – this issue is raised often but remains open. Azure also supports the syntax trigger: none to disable a pipeline – you can't do this is GitHub either, but I use the branch list [ none ] as a workaround. (Literally this means “run the workflow on pushes to the none branch”, but as long as I don't have one of those I'm good).

  • Both platforms use the jobs keyword, but Azure allows you to omit it in single-job pipelines. This is less verbose, although arguably makes pipelines harder to read. Similarly Azure permits single-job pipelines to omit the job keyword, while GitHub requires each job to be explicitly and uniquely identified (GitHub's jobs collection is a dictionary while Azure's is a list).

    Jobs can be grouped into stages in Azure – there's no parallel concept for that in GitHub. This makes managing some processes trickier, for example approving deployments.

    The absence of approvals is raised in the manual trigger issue. A workaround devised by Aaron Powell implements each stage in a separate workflow – a stage requiring approval creates a GitHub issue at the end of its workflow, then issue labels are used to indicate approval and control execution of the workflow for the next stage.

  • Azure pipeline jobs are executed on an agent allocated from a pool – GitHub refers to an agent as a runner. Both platforms provide a number of cloud-based agents/runners (and seem to share VM images), and both permit you to host your own.2) A job's agent type is indicated by the pool vmImage in Azure and its runs-on argument in GitHub.

  • steps are a concept common to both platforms. Code for a step can either be supplied directly as a script or loaded from a pre-defined library. Steps can be given more readable names, but only GitHub allows name to be a step's first argument.

    In Azure, script executes a command in the agent's default shell – other shells can be specified explicitly e.g. with keywords pwsh or bash. GitHub's equivalent is run – without arguments it too uses the agent's default shell, but alternatives can be specified using its shell argument.

    Pre-defined libraries are called tasks in Azure and actions in GitHub. The GitHub equivalent to Azure's task keyword is uses. Both platforms provide a number of built-in tasks and host marketplaces for community extensions.

  • The Git repo containing an Azure Pipeline definition is automatically checked out when the pipeline is executed – not so in GitHub. The additional actions/checkout@v2 action step is necessary to allow me to use the contents of the repo.

I've laid out the Azure pipeline and GitHub workflow side-by-side here for direct comparison (you might need to be on a full size screen to see the effect). I've included non-mandatory keywords in the Azure pipeline, showing how alike the two syntax structures are.

name: AzurePipelines-HelloWorld

trigger: 
  branches:
    include: [ main ]
    
jobs:
- job: examplejob
  pool:  
    vmImage: ubuntu-latest

  steps:
  
  - pwsh: Write-Host "Hello from Azure..."
    displayName: Say hello
 
 
  # (repo already checked out automatically)
 
 
  - bash: ls -la $(System.DefaultWorkingDir…
    displayName: Inspect GitHub workflows
 
    
  - task: UseDotNet@2
    displayName: Use .NET Core
    inputs:
      version: 3.1.x
name: GitHubActions-HelloWorld

on:
  push:
    branches: [ main ]

jobs:
  examplejob:
  
    runs-on: ubuntu-latest

    steps:

    - name: Say hello
      run: Write-Host "Hello from GitHub..."
      shell: pwsh
      
    - name: Checkout repo
      uses: actions/checkout@v2
      
    - name: Inspect GitHub workflows
      run: ls -la $GITHUB_WORKSPACE/.github…
      shell: bash
      
    - name: Use .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.x

The major differences I see here are in syntax structure and support around triggers. On the whole, GitHub's syntax enforces more consistency, with the result that workflows are easier to read:

  • the single on keyword groups all execution triggers into a single place
  • required container elements (like jobs and job_id) make workflow structure explicit
  • allowing run or uses to be preceded by name makes natural-language action descriptions easy to find.

Trigger support in Azure is more flexible – it's possible to disable a single pipeline, run one manually and use stages to require approvals. From the associated GitHub issue it's not only Azure DevOps users who miss this functionality.

From my Microsoft-focussed perspective, a practical deficiency of GitHub is the number of ready-rolled tasks available for Visual Studio and Azure, but I suspect it's only a matter of time. Microsoft's growing GitHub Actions library (and the fact that it owns GitHub!) make me wonder if we won't all end up here eventually.

This table maps corresponding concepts and keywords across the two platforms.

Azure Pipelines GitHub Actions
Concept Keyword Concept Keyword
Pipeline Workflow
Trigger trigger/pr/schedules Event on (with <event_name> argument)
Stage stage Not supported
Job job (optional) Job <job_id> (mandatory)
Dependency demands Dependency needs
Execution condition condition Execution condition if
Agent (from pool) pool Runner runs-on
Script script/bash/powershell/pwsh Script run (with shell argument)
Task task Action uses
Display name displayName Display name name (can be first task/script argument)
Task-specific parameters inputs Action-specific parameters with
Execution condition condition Execution condition if

Complete syntax guides are available for both Azure Pipelines and GitHub Actions.

If you found this article useful, please share it!


1)
OK, unless you're still using the classic editor :-P
2)
A neat use case I saw for this at GitHub Satellite was Arduino's use of self-hosted runners to perform integration tests of microcontroller boards and software.
M W J K E