fix: resolve YAML anchors and aliases in Actions workflows (#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.

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
Signed-off-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind authored and GitHub committed 2026-08-20 05:03:10 +00:00
1 parent 89b891b168
commit ed4d7ea08d
11 files changed
+240 -38

No files matched your search

+2 -3
View File
@@ -4,13 +4,11 @@
package actions
import (
"bytes"
"context"
"fmt"
"slices"
"strings"
"gitea.dev/actionslib/pkg/model"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
@@ -20,6 +18,7 @@ import (
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
actions_module "gitea.dev/modules/actions"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/json"
@@ -553,7 +552,7 @@ func handleSchedules(
crons := make([]*actions_model.ActionSchedule, 0, len(detectedWorkflows))
for _, dwf := range detectedWorkflows {
// Check cron job condition. Only working in default branch
workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content))
workflow, err := jobparser.ReadWorkflow(dwf.Content)
if err != nil {
log.Error("ReadWorkflow: %v", err)
continue
+5 -9
View File
@@ -40,17 +40,13 @@ func parseRawPermissionsExplicit(rawPerms *yaml.Node) *repo_model.ActionsTokenPe
return nil
}
// Unwrap DocumentNode and resolve AliasNode
// Unwrap DocumentNode
node := rawPerms
for node.Kind == yaml.DocumentNode || node.Kind == yaml.AliasNode {
if node.Kind == yaml.DocumentNode {
if len(node.Content) == 0 {
return nil
}
node = node.Content[0]
} else {
node = node.Alias
for node.Kind == yaml.DocumentNode {
if len(node.Content) == 0 {
return nil
}
node = node.Content[0]
}
if node.Kind == yaml.ScalarNode && node.Value == "" {
+3 -5
View File
@@ -22,8 +22,6 @@ import (
"gitea.dev/modules/util"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"go.yaml.in/yaml/v4"
)
func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error {
@@ -125,12 +123,12 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
return 0, err
}
singleWorkflow := &jobparser.SingleWorkflow{}
if err := yaml.Unmarshal(content, singleWorkflow); err != nil {
workflow, err := jobparser.ReadWorkflow(content)
if err != nil {
return 0, fmt.Errorf("failed to unmarshal workflow content: %w", err)
}
// get inputs from post
workflowDispatch := singleWorkflow.WorkflowDispatchConfig()
workflowDispatch := workflow.WorkflowDispatchConfig()
if workflowDispatch == nil {
return 0, util.ErrorWrapTranslatable(
util.NewInvalidArgumentErrorf("workflow %q has no workflow_dispatch event trigger", workflowID),