Skip to content

Add YAML anchor and alias support for GitHub Actions workflows #557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,33 @@ func (p *parser) parse(n *yaml.Node) *Workflow {
// }
// }

// containsAnchorsOrAliases recursively checks if a YAML node tree contains any anchors or aliases
func containsAnchorsOrAliases(node *yaml.Node) bool {
if node.Anchor != "" || node.Alias != nil {
return true
Copy link

@ChristopherHX ChristopherHX Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had added anchor support in nektos/act yaml validator a while back at nektos/act#5893

e.g. https://github.com/nektos/act/blob/5e62222f80b8847de5dfced0527eaaeca3303168/pkg/schema/schema.go#L241C1-L243C3 I then traverse over alias and just handle it as if there were no alias.

Some suggestions from my side, using yaml.Node also heavily.

Just do *node = node.Alias and skip a lot of serializing / deserializing?

Otherwise assign node.Alias where you read this node pointer.

if node.Anchor != ""
just do node.Anchor = "" or remove the assert that require this to be cleared if any.

If I didn't produce a big bug in act, then this improves the code in my opinion

Otherwise use yaml.Node.Encode() and yaml.Node.Decode to replace marshall and unmarshall, this would avoid generating a text representation of the yaml that is not needed.

Serializing the yaml.Node into an interface may break diagnostic features of actionlint regardless if you use yaml.Node or text as medium for reading the yaml again.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for the help, those are good tips!

}

for _, child := range node.Content {
if containsAnchorsOrAliases(child) {
return true
}
}

return false
}

// resolveYAMLAnchors resolves YAML anchors by using the two-pass strategy:
// 1. Parse into interface{} to let yaml.v3 resolve anchors automatically
// 2. Marshal back to YAML to get the resolved structure
func resolveYAMLAnchors(b []byte) ([]byte, error) {
var data interface{}
if err := yaml.Unmarshal(b, &data); err != nil {
return nil, err
}

return yaml.Marshal(data)
}

func handleYAMLError(err error) []*Error {
re := regexp.MustCompile(`\bline (\d+):`)

Expand Down Expand Up @@ -1417,12 +1444,32 @@ func handleYAMLError(err error) []*Error {
// detected while parsing the input. It means that detecting one error does not stop parsing. Even
// if one or more errors are detected, parser will try to continue parsing and finding more errors.
func Parse(b []byte) (*Workflow, []*Error) {
// First, check if the YAML contains anchors/aliases
var n yaml.Node

if err := yaml.Unmarshal(b, &n); err != nil {
return nil, handleYAMLError(err)
}

// If the YAML contains anchors or aliases, resolve them
if containsAnchorsOrAliases(&n) {
resolvedBytes, err := resolveYAMLAnchors(b)
if err != nil {
return nil, []*Error{{
Message: fmt.Sprintf("could not resolve YAML anchors: %s", err.Error()),
Filepath: "",
Line: 1,
Column: 1,
Kind: "syntax-check",
}}
}
b = resolvedBytes

// Re-parse the resolved YAML
if err := yaml.Unmarshal(b, &n); err != nil {
return nil, handleYAMLError(err)
}
}

// Uncomment for checking YAML tree
// dumpYAML(&n, 0)

Expand Down
Loading