From 0862d232f88085293157dfa86627e6db5410b63f Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 5 Apr 2026 22:08:13 +0200 Subject: [PATCH 1/5] Report structurally invalid workflows to users (#37115) `model.ReadWorkflow` succeeds for YAML that is syntactically valid but fails deeper parsing in `jobparser.Parse` (e.g. blank lines inside `run: |` blocks cause a SetJob round-trip error). Add `ValidateWorkflowContent` which runs the full `jobparser.Parse` to catch these cases, and use it in the file view, the actions workflow list, and the workflow detection loop so users see the error instead of silently getting a 500 or a dropped workflow. Co-Authored-By: Claude Sonnet 4.6 --- modules/actions/workflows.go | 13 +++++++++++++ routers/web/repo/actions/actions.go | 5 +++++ routers/web/repo/view_file.go | 4 +--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 4ac06def4d5a1..83bbfa3a1b87d 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -107,6 +107,13 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { return events, nil } +// ValidateWorkflowContent catches structural errors (e.g. blank lines in run: | blocks) +// that model.ReadWorkflow alone does not detect. +func ValidateWorkflowContent(content []byte) error { + _, err := jobparser.Parse(content) + return err +} + func DetectWorkflows( gitRepo *git.Repository, commit *git.Commit, @@ -129,6 +136,9 @@ func DetectWorkflows( // one workflow may have multiple events events, err := GetEventsFromContent(content) + if err == nil { + err = ValidateWorkflowContent(content) + } if err != nil { log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) continue @@ -173,6 +183,9 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D // one workflow may have multiple events events, err := GetEventsFromContent(content) + if err == nil { + err = ValidateWorkflowContent(content) + } if err != nil { log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) continue diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 988d2d0a993b9..7d373c79ed6b7 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -151,6 +151,11 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow workflows = append(workflows, workflow) continue } + if err := actions.ValidateWorkflowContent(content); err != nil { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) + workflows = append(workflows, workflow) + continue + } workflow.Workflow = wf // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. hasJobWithoutNeeds := false diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 3ae0dab25b8a3..fe727b23da3a3 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -26,7 +26,6 @@ import ( "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" - "github.com/nektos/act/pkg/model" ) func prepareLatestCommitInfo(ctx *context.Context) bool { @@ -184,8 +183,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { if err != nil { log.Error("actions.GetContentFromEntry: %v", err) } - _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content)) - if workFlowErr != nil { + if workFlowErr := actions.ValidateWorkflowContent(content); workFlowErr != nil { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } } else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) { From 6d83eb3b95de55cde3dcc9dd3edb982064cb92ee Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 6 Apr 2026 12:21:07 +0200 Subject: [PATCH 2/5] format --- routers/web/repo/view_file.go | 1 - 1 file changed, 1 deletion(-) diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index fe727b23da3a3..65fcb8adba233 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -25,7 +25,6 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" - ) func prepareLatestCommitInfo(ctx *context.Context) bool { From 102a93cafe057f2441411e0be44034b5c962810d Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 8 Apr 2026 18:56:38 +0200 Subject: [PATCH 3/5] fix --- modules/actions/workflows.go | 9 +++------ modules/actions/workflows_test.go | 8 ++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 83bbfa3a1b87d..ba1aee7d72f05 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -103,6 +103,9 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { if err != nil { return nil, err } + if err := ValidateWorkflowContent(content); err != nil { + return nil, err + } return events, nil } @@ -136,9 +139,6 @@ func DetectWorkflows( // one workflow may have multiple events events, err := GetEventsFromContent(content) - if err == nil { - err = ValidateWorkflowContent(content) - } if err != nil { log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) continue @@ -183,9 +183,6 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D // one workflow may have multiple events events, err := GetEventsFromContent(content) - if err == nil { - err = ValidateWorkflowContent(content) - } if err != nil { log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) continue diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index ea027366f7eb5..675b26e8c1daa 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -14,6 +14,10 @@ import ( "github.com/stretchr/testify/assert" ) +func fullWorkflowContent(yamlOn string) []byte { + return []byte("name: test\n" + yamlOn + "\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo hello\n") +} + func TestIsWorkflow(t *testing.T) { oldDirs := setting.Actions.WorkflowDirs defer func() { @@ -218,7 +222,7 @@ func TestDetectMatched(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - evts, err := GetEventsFromContent([]byte(tc.yamlOn)) + evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0])) @@ -373,7 +377,7 @@ func TestMatchIssuesEvent(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - evts, err := GetEventsFromContent([]byte(tc.yamlOn)) + evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) From dbedc2d254e51cbc2ccaef3e10a83614939402ec Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 8 Apr 2026 20:05:49 +0200 Subject: [PATCH 4/5] Update routers/web/repo/actions/actions.go Co-authored-by: Zettat123 Signed-off-by: Nicolas --- routers/web/repo/actions/actions.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 7d373c79ed6b7..644a53f28a0fc 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -320,6 +320,10 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) { if !job.Status.IsWaiting() { continue } + if err := actions.ValidateWorkflowContent(job.WorkflowPayload); err != nil { + runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) + break + } hasOnlineRunner := false for _, runner := range runners { if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) { From 128a21168e2a74cf2f33363cb348eef7d4837453 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 9 Apr 2026 09:32:12 +0800 Subject: [PATCH 5/5] clarify test string --- modules/actions/workflows_test.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index 675b26e8c1daa..cda2de13e28af 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -9,20 +9,26 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/stretchr/testify/assert" ) -func fullWorkflowContent(yamlOn string) []byte { - return []byte("name: test\n" + yamlOn + "\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo hello\n") +func fullWorkflowContent(part string) []byte { + return []byte(` +name: test +` + part + ` +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo hello +`) } func TestIsWorkflow(t *testing.T) { - oldDirs := setting.Actions.WorkflowDirs - defer func() { - setting.Actions.WorkflowDirs = oldDirs - }() + defer test.MockVariableValue(&setting.Actions.WorkflowDirs)() tests := []struct { name string