This is the fourth and final part of the blog series on how to move from Azure Pipelines to GitHub Actions.

This is a multi part article - find the other parts at the links below:

Table of Contents

  1. Templates
    1. Azure Pipelines
    2. GitHub Actions
  2. Conclusion
  3. One more thing
  4. Examples

Templates

Templates are great to reuse pipelines and avoid redundant work. You can declare necessary steps and reuse them in several pipelines. I love this feature and it was the one feature, that stopped me from moving all my pipelines to GitHub actions. But this has been taken care of and we are good to go 😉

Azure Pipelines

In Azure Pipelines, templates can be used for all scopes: steps, jobs, stages. You can declare:

  • several steps in a template and call them from a job
  • entire jobs or stages
  • call templates within templates

From a folder structure perspective, I create a folder templates within the .azuredevops folder in the root of the repository. This folder contains three sub-folders:

  • steps
  • jobs
  • stages

and each folder contains the actual template files.

If you want to pass parameters to templates, you have to use the parameters key word and declare them as shown below.

Here is an example with steps (.azuredevops/templates/steps/copy_files_build_image.yaml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
parameters:
- name: container_registry
- name: image_repository_name

steps:
- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
Write-Host "Registry: $env:container_registry"
Write-Host "Registry Repo: $env:image_repository_name"
pwsh: true
env:
container_registry: ${{ parameters.container_registry }}
image_repository_name: ${{ parameters.image_repository_name }}

- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
Write-Host "Registry: $env:container_registry"
Write-Host "Registry Repo: $env:image_repository_name"
pwsh: true
env:
container_registry: ${{ parameters.container_registry }}
image_repository_name: ${{ parameters.image_repository_name }}

You can then use the template within the pipeline:
(Pipelines are stored in the .azuredevops/pipelines/ directory)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trigger: none

stages:
- stage: build
pool:
vmImage: ubuntu-latest
jobs:
- job: build
steps:
- checkout: self

- template: ../templates/steps/copy_files_build_image.yaml
parameters:
container_registry: 'compregistrydevcr'
image_repository_name: 'frontend'

You can find more examples in my GitHub repo here.

GitHub Actions

Github Actions has a slightly different apporach but all in all, it accomplishes the same result. The official name for this feature is Reusable Workflows and you can read more about it here.

The terms used by GitHub are as followed:

  • the template is the “called” workflow
  • the workflow calling the template is the “caller” workflow

The term template is used by me to refer to the called workflow.
The only difference between the two is the trigger. The template must have the following trigger:

1
2
on:
workflow_call:

Here you can define the parameters you want to pass to the template:

1
2
3
4
5
6
7
8
9
10
11
12
on:
workflow_call:
inputs:
container_registry:
required: true
type: string
image_repository_name:
required: true
type: string
secrets:
password:
required: true

Another difference is, you can differentiate between regular parameters (or inputs) and secrets. Secrets are treated differently, because the content will never be printed to the logs, if the are referenced in outputs.

You can use the template with the following example:

1
2
3
4
5
6
7
8
9
...
jobs:
build:
uses: chrburmeister/azure-pipelines-to-github-actions/.github/workflows/template_steps_part1.yml@main
with:
container_registry: 'compregistrydevcr'
image_repository_name: 'frontend'
secrets:
password: ${{ secrets.PASSWORD }}

The uses clause references the file as followed:
<organization name>\<repository name>\<entire path to the file>@<branch name>
The with clause contains the inputs and the secrets part the secrets.

You have to write entire jobs in a template and also create a job to call it within the caller workflow, which can be a bit confusing. Therefore, you cannot create something like in Azure Pipelines, in which you can call a template within a template. I think, this makes in a little cleaner, because of less clutter.

Conclusion

I hope, you now have a better understanding of GitHub Actions and how it differs from Azure Piplines. Personally, I like GitHub Actions a litte better, just because there is less cluter overall and this makes it cleaner. Also, the possiblity to automate tasks related to issues and projects is really nice.
Of course, I wasn’t able to cover every aspect of the migration, but this should get you started 😉

One more thing

GitHub offers a way to migrate from Azure DevOps to GitHub - using the GitHub CLI and another CLI - ado2gh, however, this tool only migrate repos, boards and can point the pipelines to use GitHub instead of Azure Repos. If you want switch entirely to GitHub, you have to rewrite the pipelines yourself.

Examples

I wrote some examples, check out the following GitHub repository.