Templates in ADO vs GHA
It's been over a year now since I switched from Azure DevOps to GitHub Actions for our monorepo CI/CD pipelines. For the most part, the two platforms have equivalent features -- not the same, but close enough that you can implement a feature you want in either. I think the thing that disappoints me most about GitHub Actions is the lack of real Azure-style "templates".
In GitHub Actions, you have two ways to share snippets between pipelines -- reusable actions and reusable workflows.
GitHub also supports reusable workflows, which behave similarly except they can define multiple jobs or even job matrices to run, and provide many more security and control features, which is nice if you need to be careful about what secrets or settings they should be able to access in the calling repo. However, today, these workflows have some strict usage limits (20 separate workflow calls total, 4 levels nesting). These limits (along with the 256-node fanout limit for matrix jobs) aren't an issue for most open-source single-project repos, but for large monorepos with many different capabilities, these restrictions force you to constantly question whether a given implementation is feasible or you need to use a different pattern.
But the ins and outs of actions and workflows aren't my biggest gripe about GitHub. My big gripe is the lack of templates. In Azure DevOps, a
template: keyword can be used all over your YAML file. (Not anywhere, there are some restrictions what can be templated in Azure as well, but for most users it is virtually anywhere.)
For example, in Azure, I can define a tiny YAML file that looks like this:
# Global variables (example)
- name: PrimaryNodeVersion
- name: PrimaryLinuxPool
value: 'Self-Hosted Linux 2023.09.01'
- name: PrimaryMacPool
value: 'Self-Hosted Mac 2023.09.01'
- name: PrimaryWindowsPool
value: 'Self-Hosted Windows 2023.09.01'
- name: FORCE_COLOR
Then, in any pipeline, I can insert this file into the top like so:
- job: build_xbox
- template: ../global/_variables.yaml
This allows, for example, defining in a single spot the latest pool name / tag name of your intended runners, or intended toolchain versions, and so on, and updating just one line to adjust all pipelines in the repository. This doesn't exist in GitHub Actions, as reusable actions are too low-level, and reusable workflows are too high-level.
The equivalent of GitHub's reusable actions in Azure would be a template YAML file that defines steps, such as:
- script: 'node common/scripts/install-run-rush.js prettify --mode=check --base=origin/main'
- script: 'node common/scripts/install-run-rush.js install'
- script: 'node common/scripts/install-run-rush.js test'
Then, once again, you can insert the contents of this file into any pipeline in the desired spot:
- job: build_xbox
- checkout: self
- template: _build.yaml
The big advantage Azure has over GitHub in this case is that everything inside the reusable action appears as a single "step" in the calling job in GitHub; in Azure, it still shows each individual step as it is executing. For large reusable actions this becomes very cumbersome... although, if they fixed the "no log history at load time" issue, that would mitigate it somewhat.
It'd be unfair to claim Azure is perfect -- some features (such as the 3 different types of template expressions) have a steep learning curve and are easy to get wrong at first. And, GitHub is definitely "workable" for a monorepo -- at least at our size. However, GitHub's restrictive usage limits and missing features (like templates) prevent it from being great.