From 21e7eef83b6283661a3c33455a1c6d026d910705 Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:54:07 +0800 Subject: [PATCH 01/11] fix --- services/mailer/mail_workflow_run.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/services/mailer/mail_workflow_run.go b/services/mailer/mail_workflow_run.go index 29b3abda8ee29..4602e2b6abcff 100644 --- a/services/mailer/mail_workflow_run.go +++ b/services/mailer/mail_workflow_run.go @@ -34,6 +34,17 @@ func generateMessageIDForActionsWorkflowRunStatusEmail(repo *repo_model.Reposito } func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, sender *user_model.User, recipients []*user_model.User) { + jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + if err != nil { + log.Error("GetRunJobsByRunID: %v", err) + return + } + for _, job := range jobs { + if !job.Status.IsDone() { + return + } + } + subject := "Run" switch run.Status { case actions_model.StatusFailure: @@ -48,11 +59,6 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo messageID := generateMessageIDForActionsWorkflowRunStatusEmail(repo, run) metadataHeaders := generateMetadataHeaders(repo) - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) - if err != nil { - log.Error("GetRunJobsByRunID: %v", err) - return - } sort.SliceStable(jobs, func(i, j int) bool { si, sj := jobs[i].Status, jobs[j].Status /* From 0ec1bb3bbadd0e51db05531948dd6cbbe1ed76ea Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:24:18 +0800 Subject: [PATCH 02/11] add trace --- services/mailer/mail_workflow_run.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/mailer/mail_workflow_run.go b/services/mailer/mail_workflow_run.go index 4602e2b6abcff..ee41a3f03ad82 100644 --- a/services/mailer/mail_workflow_run.go +++ b/services/mailer/mail_workflow_run.go @@ -122,6 +122,7 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo } msgs := make([]*sender_service.Message, 0, len(tos)) for _, rec := range tos { + log.Trace("Composing actions email and send to %s (UID: %d)", rec.Name, rec.ID) msg := sender_service.NewMessageFrom( rec.Email, displayName, @@ -166,6 +167,7 @@ func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo } if len(recipients) > 0 { + log.Trace("MailActionsTrigger: will try to send actions email") composeAndSendActionsWorkflowRunStatusEmail(ctx, repo, run, sender, recipients) } } From 3957352ecdf8020d5809551f7f4577d3cda551be Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:29:08 +0800 Subject: [PATCH 03/11] add trace --- services/mailer/mail_workflow_run.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/mailer/mail_workflow_run.go b/services/mailer/mail_workflow_run.go index ee41a3f03ad82..38603be83324f 100644 --- a/services/mailer/mail_workflow_run.go +++ b/services/mailer/mail_workflow_run.go @@ -122,7 +122,7 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo } msgs := make([]*sender_service.Message, 0, len(tos)) for _, rec := range tos { - log.Trace("Composing actions email and send to %s (UID: %d)", rec.Name, rec.ID) + log.Trace("Composing actions email and sending to %s (UID: %d)", rec.Name, rec.ID) msg := sender_service.NewMessageFrom( rec.Email, displayName, @@ -167,7 +167,6 @@ func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo } if len(recipients) > 0 { - log.Trace("MailActionsTrigger: will try to send actions email") composeAndSendActionsWorkflowRunStatusEmail(ctx, repo, run, sender, recipients) } } From cdb1e80bf95c5d1f35c2b4839621e32d9704bb25 Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:02:31 +0800 Subject: [PATCH 04/11] assume --- routers/web/repo/actions/view.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 52b2e9995e3ba..020d881e49e58 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -560,9 +560,9 @@ func Cancel(ctx *context_module.Context) { if len(updatedjobs) > 0 { job := updatedjobs[0] actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } - ctx.JSON(http.StatusOK, struct{}{}) + + ctx.JSONOK() } func Approve(ctx *context_module.Context) { @@ -603,18 +603,16 @@ func Approve(ctx *context_module.Context) { actions_service.CreateCommitStatus(ctx, jobs...) - if len(updatedjobs) > 0 { - job := updatedjobs[0] - actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) - } - for _, job := range updatedjobs { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } + if len(updatedjobs) > 0 { + job := updatedjobs[0] + actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) + } - ctx.JSON(http.StatusOK, struct{}{}) + ctx.JSONOK() } func Delete(ctx *context_module.Context) { From 7384a652ebb3ce767df57a8b6f14979b1fcdc6d4 Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:05:45 +0800 Subject: [PATCH 05/11] add trace --- services/mailer/mail_workflow_run.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/mailer/mail_workflow_run.go b/services/mailer/mail_workflow_run.go index 38603be83324f..59203c5d947ec 100644 --- a/services/mailer/mail_workflow_run.go +++ b/services/mailer/mail_workflow_run.go @@ -41,6 +41,7 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo } for _, job := range jobs { if !job.Status.IsDone() { + log.Trace("composeAndSendActionsWorkflowRunStatusEmail: A job is not done. Will not compose and send actions email.") return } } @@ -122,7 +123,7 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo } msgs := make([]*sender_service.Message, 0, len(tos)) for _, rec := range tos { - log.Trace("Composing actions email and sending to %s (UID: %d)", rec.Name, rec.ID) + log.Trace("Sending actions email to %s (UID: %d)", rec.Name, rec.ID) msg := sender_service.NewMessageFrom( rec.Email, displayName, @@ -167,6 +168,7 @@ func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo } if len(recipients) > 0 { + log.Trace("MailActionsTrigger: Initiate email composition") composeAndSendActionsWorkflowRunStatusEmail(ctx, repo, run, sender, recipients) } } From 1d9c378cc84f7c613c2d453b4434f2afd1f52450 Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:02:03 +0800 Subject: [PATCH 06/11] update --- services/actions/clear_tasks.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index 274c04aa57f08..139f3e2a53433 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -42,10 +42,8 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } - if len(jobs) > 0 { - job := jobs[0] - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) - } + job := jobs[0] + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } } From c3a7f15bc4c4e83d533f7960f6752adc0cadb1a8 Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:51:16 +0800 Subject: [PATCH 07/11] Revert "assume" This reverts commit cdb1e80bf95c5d1f35c2b4839621e32d9704bb25. --- routers/web/repo/actions/view.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 020d881e49e58..52b2e9995e3ba 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -560,9 +560,9 @@ func Cancel(ctx *context_module.Context) { if len(updatedjobs) > 0 { job := updatedjobs[0] actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } - - ctx.JSONOK() + ctx.JSON(http.StatusOK, struct{}{}) } func Approve(ctx *context_module.Context) { @@ -603,16 +603,18 @@ func Approve(ctx *context_module.Context) { actions_service.CreateCommitStatus(ctx, jobs...) - for _, job := range updatedjobs { - _ = job.LoadAttributes(ctx) - notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) - } if len(updatedjobs) > 0 { job := updatedjobs[0] actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } - ctx.JSONOK() + for _, job := range updatedjobs { + _ = job.LoadAttributes(ctx) + notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + } + + ctx.JSON(http.StatusOK, struct{}{}) } func Delete(ctx *context_module.Context) { From bc3a4674ea8a588af962edfb06d3861731d3c36b Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Wed, 6 Aug 2025 15:31:57 +0200 Subject: [PATCH 08/11] remove duplicate workflow run trigger + add test for cancel * missing test for approve --- routers/web/repo/actions/view.go | 2 - tests/integration/repo_webhook_test.go | 128 +++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 52b2e9995e3ba..68f7d246e7ae1 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -560,7 +560,6 @@ func Cancel(ctx *context_module.Context) { if len(updatedjobs) > 0 { job := updatedjobs[0] actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } ctx.JSON(http.StatusOK, struct{}{}) } @@ -606,7 +605,6 @@ func Approve(ctx *context_module.Context) { if len(updatedjobs) > 0 { job := updatedjobs[0] actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } for _, job := range updatedjobs { diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 1da7bc9d3c80f..52832330a9c3d 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -12,6 +12,7 @@ import ( "path" "strings" "testing" + "time" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/repo" @@ -1058,6 +1059,10 @@ func Test_WebhookWorkflowRun(t *testing.T) { name: "WorkflowRunDepthLimit", callback: testWebhookWorkflowRunDepthLimit, }, + { + name: "WorkflowRunDuplicateEvents", + callback: testWorkflowRunDuplicateEvents, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -1070,6 +1075,129 @@ func Test_WebhookWorkflowRun(t *testing.T) { } } +func testWorkflowRunDuplicateEvents(t *testing.T, webhookData *workflowRunWebhook) { + // 1. create a new webhook with special webhook for repo1 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", webhookData.URL, "workflow_run") + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + + gitRepo1, err := gitrepo.OpenRepository(t.Context(), repo1) + assert.NoError(t, err) + + // 2.2 trigger the webhooks + + // add workflow file to the repo + // init the workflow + wfTreePath := ".gitea/workflows/push.yml" + wfFileContent := `on: + push: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: exit 0 + + test2: + needs: [test] + runs-on: ubuntu-latest + steps: + - run: exit 0 + + test3: + needs: [test, test2] + runs-on: ubuntu-latest + steps: + - run: exit 0 + + test4: + needs: [test, test2, test3] + runs-on: ubuntu-latest + steps: + - run: exit 0 + + test5: + needs: [test, test2, test4] + runs-on: ubuntu-latest + steps: + - run: exit 0 + + test6: + strategy: + matrix: + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04] + needs: [test, test2, test3] + runs-on: ${{ matrix.os }} + steps: + - run: exit 0 + + test7: + needs: test6 + runs-on: ubuntu-latest + steps: + - run: exit 0 + + test8: + runs-on: ubuntu-latest + steps: + - run: exit 0 + + test9: + strategy: + matrix: + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, ubuntu-25.04, windows-2022, windows-2025, macos-13, macos-14, macos-15] + runs-on: ${{ matrix.os }} + steps: + - run: exit 0 + + test10: + runs-on: ubuntu-latest + steps: + - run: exit 0` + opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent) + createWorkflowFile(t, token, "user2", "repo1", wfTreePath, opts) + + commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch) + assert.NoError(t, err) + + // 3. validate the webhook is triggered + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + assert.Len(t, webhookData.payloads, 1) + assert.Equal(t, "requested", webhookData.payloads[0].Action) + assert.Equal(t, "queued", webhookData.payloads[0].WorkflowRun.Status) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[0].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[0].WorkflowRun.HeadSha) + assert.Equal(t, "repo1", webhookData.payloads[0].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[0].Repo.FullName) + + time.Sleep(15 * time.Second) // wait for the workflow to be processed + + // Call cancel ui api + // Only a web UI API exists for cancelling workflow runs, so use the UI endpoint. + cancelURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/cancel", webhookData.payloads[0].WorkflowRun.RunNumber) + req := NewRequestWithValues(t, "POST", cancelURL, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + session.MakeRequest(t, req, http.StatusOK) + + assert.Len(t, webhookData.payloads, 2) + + // 4. Validate the second webhook payload + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + assert.Equal(t, "completed", webhookData.payloads[1].Action) + assert.Equal(t, "push", webhookData.payloads[1].WorkflowRun.Event) + assert.Equal(t, "completed", webhookData.payloads[1].WorkflowRun.Status) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[1].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[1].WorkflowRun.HeadSha) + assert.Equal(t, "repo1", webhookData.payloads[1].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[1].Repo.FullName) +} + func testWebhookWorkflowRun(t *testing.T, webhookData *workflowRunWebhook) { // 1. create a new webhook with special webhook for repo1 user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) From cf547bb15d64a2f9fc7d204f1c0622fd61a8f38c Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Wed, 6 Aug 2025 21:44:25 +0800 Subject: [PATCH 09/11] update --- routers/web/repo/actions/view.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 68f7d246e7ae1..e0565c0de5695 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -561,7 +561,7 @@ func Cancel(ctx *context_module.Context) { job := updatedjobs[0] actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) } - ctx.JSON(http.StatusOK, struct{}{}) + ctx.JSONOK() } func Approve(ctx *context_module.Context) { @@ -612,7 +612,7 @@ func Approve(ctx *context_module.Context) { notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } - ctx.JSON(http.StatusOK, struct{}{}) + ctx.JSONOK() } func Delete(ctx *context_module.Context) { From e212011fcab1377da21a9bfa204ef517c1a47108 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 8 Aug 2025 22:49:08 +0200 Subject: [PATCH 10/11] remove timeout --- tests/integration/repo_webhook_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 2a14119da27aa..4a24f4eb769e4 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -12,7 +12,6 @@ import ( "path" "strings" "testing" - "time" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/repo" @@ -1246,8 +1245,6 @@ jobs: assert.Equal(t, "repo1", webhookData.payloads[0].Repo.Name) assert.Equal(t, "user2/repo1", webhookData.payloads[0].Repo.FullName) - time.Sleep(15 * time.Second) // wait for the workflow to be processed - // Call cancel ui api // Only a web UI API exists for cancelling workflow runs, so use the UI endpoint. cancelURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/cancel", webhookData.payloads[0].WorkflowRun.RunNumber) From 0506162f17c367df2837343289f55595491ae51d Mon Sep 17 00:00:00 2001 From: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Date: Sat, 9 Aug 2025 13:11:43 +0800 Subject: [PATCH 11/11] update --- services/mailer/mail_workflow_run.go | 2 +- services/mailer/notify.go | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/services/mailer/mail_workflow_run.go b/services/mailer/mail_workflow_run.go index 59203c5d947ec..ec6f123139db9 100644 --- a/services/mailer/mail_workflow_run.go +++ b/services/mailer/mail_workflow_run.go @@ -149,7 +149,7 @@ func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo if setting.MailService == nil { return } - if run.Status.IsSkipped() { + if !run.Status.IsDone() || run.Status.IsSkipped() { return } diff --git a/services/mailer/notify.go b/services/mailer/notify.go index c008685e131c2..ae16b2b429a93 100644 --- a/services/mailer/notify.go +++ b/services/mailer/notify.go @@ -208,8 +208,5 @@ func (m *mailNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner * } func (m *mailNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { - if !run.Status.IsDone() { - return - } MailActionsTrigger(ctx, sender, repo, run) }