mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-21 02:50:37 +00:00
d8e179f28f
Backport https://github.com/go-gitea/gitea/pull/38984 Workflows using YAML anchors are rejected as invalid, because a workflow is split into one document per job and an alias whose anchor lands in another job's document no longer resolves. Node walkers such as `on:` parsing have no alias case either. Aliases are now expanded once, right after the workflow is parsed and before anything reads or splits it, bounded like GitHub's parser so nested aliases cannot expand without limit. Merge keys stay unsupported, as they are upstream. Fixes: https://github.com/go-gitea/gitea/issues/38983
121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package jobparser
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
|
|
"gitea.com/gitea/runner/act/model"
|
|
"go.yaml.in/yaml/v4"
|
|
)
|
|
|
|
// maxExpandedNodes bounds how many nodes alias expansion may create. go-yaml's own alias guard does
|
|
// not cover us: it only counts while decoding into values, and a workflow is kept as raw yaml.Nodes.
|
|
const maxExpandedNodes = 50000
|
|
|
|
var errTooManyYamlNodes = errors.New("maximum YAML nodes exceeded")
|
|
|
|
// ReadWorkflow decodes a workflow file with its aliases expanded. Callers inspect the workflow's
|
|
// raw nodes by kind, and an alias is a kind none of them expect.
|
|
func ReadWorkflow(content []byte) (*model.Workflow, error) {
|
|
doc, err := resolveYamlAliases(content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return readWorkflowDoc(doc)
|
|
}
|
|
|
|
func readWorkflowDoc(doc *yaml.Node) (*model.Workflow, error) {
|
|
if doc.Kind == 0 {
|
|
return nil, io.EOF // what a yaml decoder reports for an empty file
|
|
}
|
|
w := new(model.Workflow)
|
|
return w, doc.Decode(w)
|
|
}
|
|
|
|
// decodeResolved is yaml.Unmarshal with aliases expanded first.
|
|
func decodeResolved(content []byte, out any) error {
|
|
doc, err := resolveYamlAliases(content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decodeYamlDoc(doc, out)
|
|
}
|
|
|
|
func decodeYamlDoc(doc *yaml.Node, out any) error {
|
|
if doc.Kind == 0 {
|
|
return nil // an empty document, as yaml.Unmarshal treats it
|
|
}
|
|
return doc.Decode(out)
|
|
}
|
|
|
|
// resolveYamlAliases parses content and replaces every alias with a copy of the node its anchor names.
|
|
func resolveYamlAliases(content []byte) (*yaml.Node, error) {
|
|
doc := &yaml.Node{}
|
|
if err := yaml.Unmarshal(content, doc); err != nil {
|
|
return nil, err
|
|
}
|
|
budget := maxExpandedNodes
|
|
return doc, expandAliases(doc, &budget)
|
|
}
|
|
|
|
// expandAliases replaces node's alias descendants in place.
|
|
func expandAliases(node *yaml.Node, budget *int) error {
|
|
node.Anchor = "" // a name for a node, not part of the workflow: keep it out of the payloads
|
|
if err := rejectMergeKeys(node); err != nil {
|
|
return err
|
|
}
|
|
for i, child := range node.Content {
|
|
if child.Kind != yaml.AliasNode {
|
|
if err := expandAliases(child, budget); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
copied, err := copyExpanded(child.Alias, budget)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.Content[i] = copied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// copyExpanded deep copies a node expandAliases already expanded and validated, since an anchor is
|
|
// declared before the alias naming it. An anchor aliased from inside itself is the exception, and
|
|
// recurses here until it exhausts budget.
|
|
func copyExpanded(node *yaml.Node, budget *int) (*yaml.Node, error) {
|
|
if *budget--; *budget < 0 {
|
|
return nil, errTooManyYamlNodes
|
|
}
|
|
if node.Kind == yaml.AliasNode {
|
|
return copyExpanded(node.Alias, budget)
|
|
}
|
|
|
|
copied := *node
|
|
copied.Content = make([]*yaml.Node, len(node.Content))
|
|
for i, child := range node.Content {
|
|
child, err := copyExpanded(child, budget)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.Content[i] = child
|
|
}
|
|
return &copied, nil
|
|
}
|
|
|
|
// rejectMergeKeys refuses `<<: *anchor`, same as GitHub does
|
|
func rejectMergeKeys(node *yaml.Node) error {
|
|
if node.Kind != yaml.MappingNode {
|
|
return nil
|
|
}
|
|
for i := 0; i < len(node.Content)-1; i += 2 {
|
|
if node.Content[i].Tag == "!!merge" {
|
|
return errors.New("merge keys (`<<`) are not supported, alias the whole value instead")
|
|
}
|
|
}
|
|
return nil
|
|
}
|