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
2026-08-20 07:03:10 +02:00
committed by GitHub
parent 89b891b168
commit ed4d7ea08d
11 changed files with 240 additions and 38 deletions
+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),
+2 -3
View File
@@ -5,7 +5,6 @@
package convert
import (
"bytes"
"context"
"errors"
"fmt"
@@ -16,7 +15,6 @@ import (
"strconv"
"time"
"gitea.dev/actionslib/pkg/model"
runnerv1 "gitea.dev/actionslib/runner/v1"
actions_model "gitea.dev/models/actions"
asymkey_model "gitea.dev/models/asymkey"
@@ -31,6 +29,7 @@ import (
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/actions"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/httplib"
@@ -566,7 +565,7 @@ func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, gi
content, err := actions.GetContentFromEntry(ctx, gitRepo, entry)
name := entry.Name()
if err == nil {
workflow, err := model.ReadWorkflow(bytes.NewReader(content))
workflow, err := jobparser.ReadWorkflow(content)
if err == nil {
// Only use the name when specified in the workflow file
if workflow.Name != "" {