Skip to content

Commit b2aee4b

Browse files
fix: explode yaml anchors (#5987)
* fix: explode yaml anchors * do not require code changes at several places * self referencing anchors cause error * fix
1 parent e935690 commit b2aee4b

File tree

5 files changed

+217
-0
lines changed

5 files changed

+217
-0
lines changed

pkg/model/action.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ type Action struct {
100100
}
101101

102102
func (a *Action) UnmarshalYAML(node *yaml.Node) error {
103+
// TODO enable after verifying that this runner side feature has rolled out in actions/runner
104+
// // Resolve yaml anchor aliases first
105+
// if err := resolveAliases(node); err != nil {
106+
// return err
107+
// }
103108
// Validate the schema before deserializing it into our model
104109
if err := (&schema.Node{
105110
Definition: "action-root",

pkg/model/anchors.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package model
2+
3+
import (
4+
"errors"
5+
6+
"gopkg.in/yaml.v3"
7+
)
8+
9+
func resolveAliasesExt(node *yaml.Node, path map[*yaml.Node]bool, skipCheck bool) error {
10+
if !skipCheck && path[node] {
11+
return errors.New("circular alias")
12+
}
13+
switch node.Kind {
14+
case yaml.AliasNode:
15+
aliasTarget := node.Alias
16+
if aliasTarget == nil {
17+
return errors.New("unresolved alias node")
18+
}
19+
path[node] = true
20+
*node = *aliasTarget
21+
if err := resolveAliasesExt(node, path, true); err != nil {
22+
return err
23+
}
24+
delete(path, node)
25+
26+
case yaml.DocumentNode, yaml.MappingNode, yaml.SequenceNode:
27+
for _, child := range node.Content {
28+
if err := resolveAliasesExt(child, path, false); err != nil {
29+
return err
30+
}
31+
}
32+
}
33+
return nil
34+
}
35+
36+
func resolveAliases(node *yaml.Node) error {
37+
return resolveAliasesExt(node, map[*yaml.Node]bool{}, false)
38+
}

pkg/model/anchors_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package model
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"gopkg.in/yaml.v3"
8+
)
9+
10+
func TestVerifyNilAliasError(t *testing.T) {
11+
var node yaml.Node
12+
err := yaml.Unmarshal([]byte(`
13+
test:
14+
- a
15+
- b
16+
- c`), &node)
17+
*node.Content[0].Content[1].Content[1] = yaml.Node{
18+
Kind: yaml.AliasNode,
19+
}
20+
assert.NoError(t, err)
21+
err = resolveAliases(&node)
22+
assert.Error(t, err)
23+
}
24+
25+
func TestVerifyNoRecursion(t *testing.T) {
26+
table := []struct {
27+
name string
28+
yaml string
29+
yamlErr bool
30+
anchorErr bool
31+
}{
32+
{
33+
name: "no anchors",
34+
yaml: `
35+
a: x
36+
b: y
37+
c: z
38+
`,
39+
yamlErr: false,
40+
anchorErr: false,
41+
},
42+
{
43+
name: "simple anchors",
44+
yaml: `
45+
a: &a x
46+
b: &b y
47+
c: *a
48+
`,
49+
yamlErr: false,
50+
anchorErr: false,
51+
},
52+
{
53+
name: "nested anchors",
54+
yaml: `
55+
a: &a
56+
val: x
57+
b: &b
58+
val: y
59+
c: *a
60+
`,
61+
yamlErr: false,
62+
anchorErr: false,
63+
},
64+
{
65+
name: "circular anchors",
66+
yaml: `
67+
a: &b
68+
ref: *c
69+
b: &c
70+
ref: *b
71+
`,
72+
yamlErr: true,
73+
anchorErr: false,
74+
},
75+
{
76+
name: "self-referencing anchor",
77+
yaml: `
78+
a: &a
79+
ref: *a
80+
`,
81+
yamlErr: false,
82+
anchorErr: true,
83+
},
84+
{
85+
name: "reuse snippet with anchors",
86+
yaml: `
87+
a: &b x
88+
b: &a
89+
ref: *b
90+
c: *a
91+
`,
92+
yamlErr: false,
93+
anchorErr: false,
94+
},
95+
}
96+
97+
for _, tt := range table {
98+
t.Run(tt.name, func(t *testing.T) {
99+
var node yaml.Node
100+
err := yaml.Unmarshal([]byte(tt.yaml), &node)
101+
if tt.yamlErr {
102+
assert.Error(t, err)
103+
return
104+
}
105+
assert.NoError(t, err)
106+
err = resolveAliases(&node)
107+
if tt.anchorErr {
108+
assert.Error(t, err)
109+
} else {
110+
assert.NoError(t, err)
111+
}
112+
})
113+
}
114+
}

pkg/model/workflow.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ func (w *Workflow) OnEvent(event string) interface{} {
6969
}
7070

7171
func (w *Workflow) UnmarshalYAML(node *yaml.Node) error {
72+
// Resolve yaml anchor aliases first
73+
if err := resolveAliases(node); err != nil {
74+
return err
75+
}
7276
// Validate the schema before deserializing it into our model
7377
if err := (&schema.Node{
7478
Definition: "workflow-root",
@@ -83,6 +87,10 @@ func (w *Workflow) UnmarshalYAML(node *yaml.Node) error {
8387
type WorkflowStrict Workflow
8488

8589
func (w *WorkflowStrict) UnmarshalYAML(node *yaml.Node) error {
90+
// Resolve yaml anchor aliases first
91+
if err := resolveAliases(node); err != nil {
92+
return err
93+
}
8694
// Validate the schema before deserializing it into our model
8795
if err := (&schema.Node{
8896
Definition: "workflow-root-strict",

pkg/model/workflow_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,3 +560,55 @@ jobs:
560560
_, err := ReadWorkflow(strings.NewReader(yaml), true)
561561
assert.Error(t, err, "read workflow should succeed")
562562
}
563+
564+
func TestReadWorkflow_AnchorStrict(t *testing.T) {
565+
yaml := `
566+
on: push
567+
568+
jobs:
569+
test:
570+
runs-on: &runner ubuntu-latest
571+
steps:
572+
- uses: &checkout actions/checkout@v5
573+
test2:
574+
runs-on: *runner
575+
steps:
576+
- uses: *checkout
577+
`
578+
579+
w, err := ReadWorkflow(strings.NewReader(yaml), true)
580+
assert.NoError(t, err, "read workflow should succeed")
581+
582+
for _, job := range w.Jobs {
583+
assert.Equal(t, []string{"ubuntu-latest"}, job.RunsOn())
584+
assert.Equal(t, "actions/checkout@v5", job.Steps[0].Uses)
585+
}
586+
}
587+
588+
func TestReadWorkflow_Anchor(t *testing.T) {
589+
yaml := `
590+
591+
jobs:
592+
test:
593+
runs-on: &runner ubuntu-latest
594+
steps:
595+
- uses: &checkout actions/checkout@v5
596+
test2: &job
597+
runs-on: *runner
598+
steps:
599+
- uses: *checkout
600+
- run: echo $TRIGGER
601+
env:
602+
TRIGGER: &trigger push
603+
test3: *job
604+
on: push #*trigger
605+
`
606+
607+
w, err := ReadWorkflow(strings.NewReader(yaml), false)
608+
assert.NoError(t, err, "read workflow should succeed")
609+
610+
for _, job := range w.Jobs {
611+
assert.Equal(t, []string{"ubuntu-latest"}, job.RunsOn())
612+
assert.Equal(t, "actions/checkout@v5", job.Steps[0].Uses)
613+
}
614+
}

0 commit comments

Comments
 (0)