Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
21e7eef
fix
NorthRealm Aug 5, 2025
0ec1bb3
add trace
NorthRealm Aug 5, 2025
3957352
add trace
NorthRealm Aug 5, 2025
cdb1e80
assume
NorthRealm Aug 5, 2025
7384a65
add trace
NorthRealm Aug 5, 2025
1d9c378
update
NorthRealm Aug 5, 2025
c3a7f15
Revert "assume"
NorthRealm Aug 5, 2025
bc3a467
remove duplicate workflow run trigger + add test for cancel
ChristopherHX Aug 6, 2025
cf547bb
update
NorthRealm Aug 6, 2025
8901e59
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 8, 2025
a9a826f
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 8, 2025
e212011
remove timeout
ChristopherHX Aug 8, 2025
0506162
update
NorthRealm Aug 9, 2025
6bcdd7e
fix more workflow_run completion events
ChristopherHX Aug 10, 2025
3a44248
modernize
ChristopherHX Aug 10, 2025
3d8b2f5
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 11, 2025
5eaa9c8
error logs
NorthRealm Aug 13, 2025
a7ddfc9
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 13, 2025
1539d9f
update
NorthRealm Aug 14, 2025
4e52f0f
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 14, 2025
f19e9e3
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 15, 2025
bab5ff2
update
NorthRealm Aug 15, 2025
ec62f81
Revert "update"
NorthRealm Aug 15, 2025
20b5777
update
NorthRealm Aug 15, 2025
a1e64e7
update
NorthRealm Aug 15, 2025
7bb8e16
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 15, 2025
1bc9097
Revert "update"
NorthRealm Aug 15, 2025
2330dcf
Revert "update"
NorthRealm Aug 15, 2025
bfc4f6e
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 16, 2025
341c5d8
fix maybe
NorthRealm Aug 19, 2025
846731a
wtf
NorthRealm Aug 19, 2025
7a41096
Revert "wtf"
NorthRealm Aug 19, 2025
42b6b4e
f
NorthRealm Aug 19, 2025
45dcc12
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 19, 2025
4773ea5
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 20, 2025
cef6eba
update
NorthRealm Aug 20, 2025
b7719ee
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 20, 2025
aa85d80
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 21, 2025
d00d5cf
do not explicitly expect a workflow run completion event while jobs a…
ChristopherHX Aug 21, 2025
24d46df
update
NorthRealm Aug 22, 2025
030b1de
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 22, 2025
aee9f6a
Merge branch 'main' into patch-actions-email-2
NorthRealm Aug 22, 2025
5efd4b9
UPDATE
NorthRealm Aug 22, 2025
e4a5b82
Merge branch 'main' into patch-actions-email-2
GiteaBot Aug 23, 2025
502f387
Merge branch 'main' into patch-actions-email-2
GiteaBot Aug 23, 2025
ab8abf9
Merge branch 'main' into patch-actions-email-2
GiteaBot Aug 23, 2025
a27e5bd
Merge branch 'main' into patch-actions-email-2
GiteaBot Aug 23, 2025
05b3384
Merge branch 'main' into patch-actions-email-2
GiteaBot Aug 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions routers/web/repo/actions/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,9 +560,8 @@ 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) {
Expand Down Expand Up @@ -606,15 +605,14 @@ 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 {
_ = job.LoadAttributes(ctx)
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) {
Expand Down
6 changes: 2 additions & 4 deletions services/actions/clear_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
19 changes: 14 additions & 5 deletions services/mailer/mail_workflow_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ 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() {
log.Trace("composeAndSendActionsWorkflowRunStatusEmail: A job is not done. Will not compose and send actions email.")
return
}
}

subject := "Run"
switch run.Status {
case actions_model.StatusFailure:
Expand All @@ -48,11 +60,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
/*
Expand Down Expand Up @@ -116,6 +123,7 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo
}
msgs := make([]*sender_service.Message, 0, len(tos))
for _, rec := range tos {
log.Trace("Sending actions email to %s (UID: %d)", rec.Name, rec.ID)
msg := sender_service.NewMessageFrom(
rec.Email,
displayName,
Expand Down Expand Up @@ -160,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)
}
}
128 changes: 128 additions & 0 deletions tests/integration/repo_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"path"
"strings"
"testing"
"time"

auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/repo"
Expand Down Expand Up @@ -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) {
Expand All @@ -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})
Expand Down