fix: anchor cyclic detection (#30)

Closes #25

Reviewed-on: https://gitea.com/actions-oss/act-cli/pulls/30
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
This commit is contained in:
Christopher Homberger
2026-02-02 20:43:49 +00:00
committed by ChristopherHX
parent 13e0654fd7
commit 6c827eba95
3 changed files with 98 additions and 26 deletions

View File

@@ -21,7 +21,7 @@ func resolveAliasesExt(node *yaml.Node, path map[*yaml.Node]bool, skipCheck bool
if err := resolveAliasesExt(node, path, true); err != nil { if err := resolveAliasesExt(node, path, true); err != nil {
return err return err
} }
delete(path, aliasTarget) delete(path, node)
case yaml.DocumentNode, yaml.MappingNode, yaml.SequenceNode: case yaml.DocumentNode, yaml.MappingNode, yaml.SequenceNode:
for _, child := range node.Content { for _, child := range node.Content {

View File

@@ -7,29 +7,6 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func TestVerifyCycleIsInvalid(t *testing.T) {
var node yaml.Node
err := yaml.Unmarshal([]byte(`
a: &a
ref: *b
b: &b
ref: *a
`), &node)
assert.Error(t, err)
}
func TestVerifyCycleIsInvalid2(t *testing.T) {
var node yaml.Node
err := yaml.Unmarshal([]byte(`
a: &a
ref: *a
`), &node)
assert.NoError(t, err)
err = resolveAliases(&node)
assert.Error(t, err)
}
func TestVerifyNilAliasError(t *testing.T) { func TestVerifyNilAliasError(t *testing.T) {
var node yaml.Node var node yaml.Node
err := yaml.Unmarshal([]byte(` err := yaml.Unmarshal([]byte(`
@@ -44,3 +21,94 @@ test:
err = resolveAliases(&node) err = resolveAliases(&node)
assert.Error(t, err) assert.Error(t, err)
} }
func TestVerifyNoRecursion(t *testing.T) {
table := []struct {
name string
yaml string
yamlErr bool
anchorErr bool
}{
{
name: "no anchors",
yaml: `
a: x
b: y
c: z
`,
yamlErr: false,
anchorErr: false,
},
{
name: "simple anchors",
yaml: `
a: &a x
b: &b y
c: *a
`,
yamlErr: false,
anchorErr: false,
},
{
name: "nested anchors",
yaml: `
a: &a
val: x
b: &b
val: y
c: *a
`,
yamlErr: false,
anchorErr: false,
},
{
name: "circular anchors",
yaml: `
a: &b
ref: *c
b: &c
ref: *b
`,
yamlErr: true,
anchorErr: false,
},
{
name: "self-referencing anchor",
yaml: `
a: &a
ref: *a
`,
yamlErr: false,
anchorErr: true,
},
{
name: "reuse snippet with anchors",
yaml: `
a: &b x
b: &a
ref: *b
c: *a
`,
yamlErr: false,
anchorErr: false,
},
}
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
var node yaml.Node
err := yaml.Unmarshal([]byte(tt.yaml), &node)
if tt.yamlErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
err = resolveAliases(&node)
if tt.anchorErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -614,17 +614,21 @@ jobs:
func TestReadWorkflow_Anchor(t *testing.T) { func TestReadWorkflow_Anchor(t *testing.T) {
yaml := ` yaml := `
on: push
jobs: jobs:
test: test:
runs-on: &runner ubuntu-latest runs-on: &runner ubuntu-latest
steps: steps:
- uses: &checkout actions/checkout@v5 - uses: &checkout actions/checkout@v5
test2: test2: &job
runs-on: *runner runs-on: *runner
steps: steps:
- uses: *checkout - uses: *checkout
- run: echo $TRIGGER
env:
TRIGGER: &trigger push
test3: *job
on: push #*trigger
` `
w, err := ReadWorkflow(strings.NewReader(yaml), false) w, err := ReadWorkflow(strings.NewReader(yaml), false)