Programming, philosophy, pedaling.
Sep 22, 2025
Tags:
TL;DR: for a very long time, GitHub Actions lacked support for YAML anchors.
This was a good thing. YAML anchors in GitHub Actions are (1) redundant
with existing functionality, (2) introduce a complication to the data model
that makes CI/CD human and machine comprehension harder, and (3) are
not even uniquely useful because GitHub has chosen not to support
the one feature (merge keys) that lacks a semantic equivalent in GitHub Actions.
This step backwards reinforces GitHub Actions’ status as an
insecure by default CI/CD platform by making
it harder for both humans and machines to analyze action and workflow
definitions for vulnerabilities. GitHub should immediately remove
support for YAML anchors, before adoption becomes widespread.
GitHub recently announced that YAML anchors are now supported in
GitHub Actions. That means that users can write things like this:
1
2
3
4
5
6
7
8
9
10
11
12
jobs:
job1:
env: &env_vars # Define the anchor on first use
NODE_ENV: production
DATABASE_URL: ${{ secrets.DATABASE_URL }}
steps:
- run: echo "Using production settings"
job2:
env: *env_vars # Reuse the environment variables
steps:
- run: echo "Same environment variables here"
On face value, this seems like a reasonable feature: the job and step
abstractions in GitHub Actions lend themselves to duplication, and YAML anchors
are one way to reduce that duplication.
Unfortunately, YAML anchors are a terrible tool for this job. Furthermore
(as we’ll see) GitHub’s implementation of YAML anchors is incomplete,
precluding the actual small subset of use cases where YAML anchors
are uniquely useful (but still not a good idea). We’ll see why below.
Pictured: the author’s understanding of the GitHub Actions product roadmap.
Redundancy
The simplest reason why YAML anchors are a bad idea is because they’re
redundant with other more explicit mechanisms for reducing duplication
in GitHub Actions.
GitHub’s own example above could be rewritten without YAML anchors as:
1
2
3
4
5
6
7
8
9
10
11
12
env:
NODE_ENV: production
DATABASE_URL: ${{ secrets.DATABASE_URL }}
jobs:
job1:
steps:
- run: echo "Using production settings"
job2:
steps:
- run: echo "Same environment variables here"
This version is significantly clearer, but has slightly different semantics:
all jobs inherit the workflow-level env. But this, in my opinion,
is a good thing: the need to template environment variables across a subset
of jobs suggests an architectural error in the workflow design.
In other words: if you find yourself wanting to use YAML anchors to
share “global” configuration between jobs or steps, you
probably actually want separate workflows, or at least separate jobs with
job-level env blocks.
In summary: YAML anchors further muddy the abstractions of workflows,
jobs, and steps, by introducing a cross-cutting form of global state that
doesn’t play by the rules of the rest of the system. This, to me, suggests
that the current Actions team lacks a strong set of opinions about how
GitHub Actions should be used, leading to a “kitchen sink” approach
that serves all users equally poorly.
Non-locality with full generality
As noted above: YAML anchors introduce a new form of
non-locality into GitHub Actions. Furthermore, this form
of non-locality is fully general: any YAML node can be anchored
and referenced. This is a bad idea for humans and machines alike:
-
For humans: a new form of non-locality makes it harder to preserve
local understanding of what a workflow, job, or step does: a unit
of work may now depend on any other unit of work in the same file,
including one hundreds or thousands of lines away. This makes it harder
to reason about the behavior of one’s GitHub Actions without context
switching.It would only be fair to note that GitHub Actions already has some
forms of non-locality: global contexts, scoping rules forenvblocks,
needsdependencies, step and job outputs, and so on. These can be
difficult to debug! But what sets them apart is their lack of
generality: each has precise semantics and scoping rules,
meaning that a user who understands those rules can comprehend
what a unit of work does without referencing the source of an
environment variable, output, &c. -
For machines: non-locality makes it significantly harder to write
tools that analyze (or transform) GitHub Actions workflows.The pain here boils down to the fact that YAML anchors diverge
from the one-to-one object model that GitHub Actions otherwise maps
onto.With anchors, that mapping becomes one-to-many: the same element
may appear once in the source, but multiple times in the loaded
object representation.In effect, this breaks a critical assumption that many tools
make about YAML in GitHub Actions: that an entity in the deserialized
object can be mapped back to a single concrete location in the
source YAML.This is needed to present reasonable source locations in error messages,
but it doesn’t hold if the object model doesn’t represent
anchors and references explicitly.Furthermore, this is the reality
for every YAML parser in wide use: all widespread YAML parsers
choose (reasonably) to copy anchored values into each
location where they’re referenced, meaning that the analyzing tool
cannot “see” the original element for source location purposes.I feel these pains directly: I maintain zizmor as a static analysis tool
for GitHub Actions, andzizmormakes both of these assumptions.
Moreover,zizmor’s dependencies make these assumptions:
serde_yaml(like most other YAML parsers) chooses to deserialize YAML
anchors by copying the anchored value into each location where it’s
referenced.
No merging anyways
One of the few things that make YAML anchors uniquely useful is
merge keys: a merge key allows a user to compose multiple referenced
mappings together into a single mapping.
An example from the YAML spec, which I think tidily demonstrates
both their use case and how incredibly confusing merge keys are:
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
27
---
- &CENTER { x: 1, y: 2 }
- &LEFT { x: 0, y: 2 }
- &BIG { r: 10 }
- &SMALL { r: 1 }
# All the following maps are equal:
- # Explicit keys
x: 1
y: 2
r: 10
label: center/big
- # Merge one map
: *CENTER
r: 10
label: center/big
- # Merge multiple maps
: [ *CENTER, *BIG ]
label: center/big
- # Override
: [ *BIG, *LEFT, *SMALL ]
x: 1
label: center/big
I personally find this syntax incredibly hard to read, but at least it
has a unique use case that could be useful in GitHub Actions:
composing multiple sets of environment variables together with clear
precedence rules is manifestly useful.
Except: GitHub Actions doesn’t support merge keys! They appear to be
using their own internal YAML parser that already had some degree of
support for anchors and references, but not for merge keys.
To me, this takes the situation from a set of bad technical decisions
(and lack of strong opinions around how GitHub Actions should be used)
to farce: the one thing that makes YAML anchors uniquely useful
in the context of GitHub Actions is the one thing that GitHub Actions
doesn’t support.
Summary
To summarize, I think YAML anchors in GitHub Actions are (1) redundant
with existing functionality, (2) introduce a complication to the data model
that makes CI/CD human and machine comprehension harder, and (3) are
not even uniquely useful because GitHub has chosen not to support
the one feature (merge keys) that lacks a semantic equivalent in GitHub Actions.
Of these reasons, I think (2) is the most important: GitHub Actions security
has been in the news a great deal recently, with the overwhelming consensus
being that it’s too easy to introduce vulnerabilities in (or expose
otherwise latent vulnerabilities through) GitHub Actions workflow.
For this reason, we need GitHub Actions to be easy to analyze
for humans and machine alike. In effect, this means that GitHub should
be decreasing the complexity of GitHub Actions, not increasing it.
YAML anchors are a step in the wrong direction for all of the
reasons aforementioned.
Of course, I’m not without self-interest here: I maintain a
static analysis tool for GitHub Actions, and supporting YAML anchors
is going to be an absolute royal pain in my ass. But it’s not just
me: tools like actionlint, claws, and poutine are all likely to
struggle with supporting YAML anchors, as they fundamentally alter
each tool’s relationship to GitHub Actions’ assumed data model. As-is,
this change blows a massive hole in the larger open source ecosystem’s
ability to analyze GitHub Actions for correctness and security.
All told: I strongly believe that GitHub should immediately remove support
for YAML anchors in GitHub Actions. The “good” news is that they can probably
do so with a bare minimum of user disruption, since support has only been
public for a few days and adoption is (probably) still primarily at
the single-use workflow layer and not the reusable action (or workflow) layer.
Discussions:

