From 79360e4ed1820d98143508aa1f72d8e640bf723e Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Sat, 13 Sep 2025 15:46:46 +0200 Subject: [PATCH] fix: explode yaml anchors (#126) * do not require code changes at several places --- pkg/model/anchors.go | 30 ++++++++++++++++++++++++ pkg/model/anchors_test.go | 35 +++++++++++++++++++++++++++ pkg/model/workflow.go | 6 +++++ pkg/model/workflow_test.go | 48 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 pkg/model/anchors.go create mode 100644 pkg/model/anchors_test.go diff --git a/pkg/model/anchors.go b/pkg/model/anchors.go new file mode 100644 index 00000000..e68c74a7 --- /dev/null +++ b/pkg/model/anchors.go @@ -0,0 +1,30 @@ +package model + +import ( + "errors" + + "gopkg.in/yaml.v3" +) + +// Assumes there is no cycle ensured via test TestVerifyCycleIsInvalid +func resolveAliases(node *yaml.Node) error { + switch node.Kind { + case yaml.AliasNode: + aliasTarget := node.Alias + if aliasTarget == nil { + return errors.New("unresolved alias node") + } + *node = *aliasTarget + if err := resolveAliases(node); err != nil { + return err + } + + case yaml.DocumentNode, yaml.MappingNode, yaml.SequenceNode: + for _, child := range node.Content { + if err := resolveAliases(child); err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/model/anchors_test.go b/pkg/model/anchors_test.go new file mode 100644 index 00000000..c1fce291 --- /dev/null +++ b/pkg/model/anchors_test.go @@ -0,0 +1,35 @@ +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "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 TestVerifyNilAliasError(t *testing.T) { + var node yaml.Node + err := yaml.Unmarshal([]byte(` +test: +- a +- b +- c`), &node) + *node.Content[0].Content[1].Content[1] = yaml.Node{ + Kind: yaml.AliasNode, + } + assert.NoError(t, err) + err = resolveAliases(&node) + assert.Error(t, err) +} diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index b2e79c12..e663fc9a 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -76,6 +76,9 @@ func (w *Workflow) UnmarshalYAML(node *yaml.Node) error { }).UnmarshalYAML(node); err != nil { return errors.Join(err, fmt.Errorf("actions YAML Schema Validation Error detected:\nFor more information, see: https://actions-oss.github.io/act-docs/usage/schema.html")) } + if err := resolveAliases(node); err != nil { + return err + } type WorkflowDefault Workflow return node.Decode((*WorkflowDefault)(w)) } @@ -90,6 +93,9 @@ func (w *WorkflowStrict) UnmarshalYAML(node *yaml.Node) error { }).UnmarshalYAML(node); err != nil { return errors.Join(err, fmt.Errorf("actions YAML Strict Schema Validation Error detected:\nFor more information, see: https://nektosact.com/usage/schema.html")) } + if err := resolveAliases(node); err != nil { + return err + } type WorkflowDefault Workflow return node.Decode((*WorkflowDefault)(w)) } diff --git a/pkg/model/workflow_test.go b/pkg/model/workflow_test.go index 7a98ce7c..6868cb28 100644 --- a/pkg/model/workflow_test.go +++ b/pkg/model/workflow_test.go @@ -560,3 +560,51 @@ jobs: _, err := ReadWorkflow(strings.NewReader(yaml), true) assert.Error(t, err, "read workflow should succeed") } + +func TestReadWorkflow_AnchorStrict(t *testing.T) { + yaml := ` +on: push + +jobs: + test: + runs-on: &runner ubuntu-latest + steps: + - uses: &checkout actions/checkout@v5 + test2: + runs-on: *runner + steps: + - uses: *checkout +` + + w, err := ReadWorkflow(strings.NewReader(yaml), true) + assert.NoError(t, err, "read workflow should succeed") + + for _, job := range w.Jobs { + assert.Equal(t, []string{"ubuntu-latest"}, job.RunsOn()) + assert.Equal(t, "actions/checkout@v5", job.Steps[0].Uses) + } +} + +func TestReadWorkflow_Anchor(t *testing.T) { + yaml := ` +on: push + +jobs: + test: + runs-on: &runner ubuntu-latest + steps: + - uses: &checkout actions/checkout@v5 + test2: + runs-on: *runner + steps: + - uses: *checkout +` + + w, err := ReadWorkflow(strings.NewReader(yaml), false) + assert.NoError(t, err, "read workflow should succeed") + + for _, job := range w.Jobs { + assert.Equal(t, []string{"ubuntu-latest"}, job.RunsOn()) + assert.Equal(t, "actions/checkout@v5", job.Steps[0].Uses) + } +}