mirror of
https://github.com/go-gitea/gitea
synced 2025-08-25 19:08:28 +00:00
Prevent duplicate actions email (#35215)
Trying to prevent duplicate action emails by adding an extra check on job status. --------- Signed-off-by: NorthRealm <155140859+NorthRealm@users.noreply.github.com> Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
This commit is contained in:
@@ -429,6 +429,12 @@ func Rerun(ctx *context_module.Context) {
|
|||||||
ctx.ServerError("UpdateRun", err)
|
ctx.ServerError("UpdateRun", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := run.LoadAttributes(ctx); err != nil {
|
||||||
|
ctx.ServerError("run.LoadAttributes", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
||||||
}
|
}
|
||||||
|
|
||||||
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
|
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
|
||||||
@@ -485,7 +491,6 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
|
|||||||
}
|
}
|
||||||
|
|
||||||
actions_service.CreateCommitStatus(ctx, job)
|
actions_service.CreateCommitStatus(ctx, job)
|
||||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
|
||||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -560,9 +565,8 @@ func Cancel(ctx *context_module.Context) {
|
|||||||
if len(updatedjobs) > 0 {
|
if len(updatedjobs) > 0 {
|
||||||
job := updatedjobs[0]
|
job := updatedjobs[0]
|
||||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
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) {
|
func Approve(ctx *context_module.Context) {
|
||||||
@@ -606,7 +610,6 @@ func Approve(ctx *context_module.Context) {
|
|||||||
if len(updatedjobs) > 0 {
|
if len(updatedjobs) > 0 {
|
||||||
job := updatedjobs[0]
|
job := updatedjobs[0]
|
||||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||||
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, job := range updatedjobs {
|
for _, job := range updatedjobs {
|
||||||
@@ -614,7 +617,7 @@ func Approve(ctx *context_module.Context) {
|
|||||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
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) {
|
func Delete(ctx *context_module.Context) {
|
||||||
|
@@ -42,10 +42,8 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac
|
|||||||
_ = job.LoadAttributes(ctx)
|
_ = job.LoadAttributes(ctx)
|
||||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
}
|
}
|
||||||
if len(jobs) > 0 {
|
job := jobs[0]
|
||||||
job := jobs[0]
|
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||||
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +99,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelAbandonedJobs cancels the jobs which have waiting status, but haven't been picked by a runner for a long time
|
// CancelAbandonedJobs cancels jobs that have not been picked by any runner for a long time
|
||||||
func CancelAbandonedJobs(ctx context.Context) error {
|
func CancelAbandonedJobs(ctx context.Context) error {
|
||||||
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{
|
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{
|
||||||
Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked},
|
Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked},
|
||||||
@@ -113,24 +111,40 @@ func CancelAbandonedJobs(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := timeutil.TimeStampNow()
|
now := timeutil.TimeStampNow()
|
||||||
|
|
||||||
|
// Collect one job per run to send workflow run status update
|
||||||
|
updatedRuns := map[int64]*actions_model.ActionRunJob{}
|
||||||
|
|
||||||
for _, job := range jobs {
|
for _, job := range jobs {
|
||||||
job.Status = actions_model.StatusCancelled
|
job.Status = actions_model.StatusCancelled
|
||||||
job.Stopped = now
|
job.Stopped = now
|
||||||
updated := false
|
updated := false
|
||||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped")
|
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped")
|
||||||
updated = err == nil && n > 0
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if err := job.LoadAttributes(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updated = n > 0
|
||||||
|
if updated && job.Run.Status.IsDone() {
|
||||||
|
updatedRuns[job.RunID] = job
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Warn("cancel abandoned job %v: %v", job.ID, err)
|
log.Warn("cancel abandoned job %v: %v", job.ID, err)
|
||||||
// go on
|
// go on
|
||||||
}
|
}
|
||||||
CreateCommitStatus(ctx, job)
|
CreateCommitStatus(ctx, job)
|
||||||
if updated {
|
if updated {
|
||||||
NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
|
||||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, job := range updatedRuns {
|
||||||
|
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,18 @@ func generateMessageIDForActionsWorkflowRunStatusEmail(repo *repo_model.Reposito
|
|||||||
return fmt.Sprintf("<%s/actions/runs/%d@%s>", repo.FullName(), run.Index, setting.Domain)
|
return fmt.Sprintf("<%s/actions/runs/%d@%s>", repo.FullName(), run.Index, setting.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, sender *user_model.User, recipients []*user_model.User) {
|
func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, sender *user_model.User, recipients []*user_model.User) error {
|
||||||
|
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, job := range jobs {
|
||||||
|
if !job.Status.IsDone() {
|
||||||
|
log.Debug("composeAndSendActionsWorkflowRunStatusEmail: A job is not done. Will not compose and send actions email.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
subject := "Run"
|
subject := "Run"
|
||||||
switch run.Status {
|
switch run.Status {
|
||||||
case actions_model.StatusFailure:
|
case actions_model.StatusFailure:
|
||||||
@@ -48,11 +59,6 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo
|
|||||||
messageID := generateMessageIDForActionsWorkflowRunStatusEmail(repo, run)
|
messageID := generateMessageIDForActionsWorkflowRunStatusEmail(repo, run)
|
||||||
metadataHeaders := generateMetadataHeaders(repo)
|
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 {
|
sort.SliceStable(jobs, func(i, j int) bool {
|
||||||
si, sj := jobs[i].Status, jobs[j].Status
|
si, sj := jobs[i].Status, jobs[j].Status
|
||||||
/*
|
/*
|
||||||
@@ -111,11 +117,11 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo
|
|||||||
"Jobs": convertedJobs,
|
"Jobs": convertedJobs,
|
||||||
"locale": locale,
|
"locale": locale,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error("ExecuteTemplate [%s]: %v", tplWorkflowRun, err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
msgs := make([]*sender_service.Message, 0, len(tos))
|
msgs := make([]*sender_service.Message, 0, len(tos))
|
||||||
for _, rec := range tos {
|
for _, rec := range tos {
|
||||||
|
log.Trace("Sending actions email to %s (UID: %d)", rec.Name, rec.ID)
|
||||||
msg := sender_service.NewMessageFrom(
|
msg := sender_service.NewMessageFrom(
|
||||||
rec.Email,
|
rec.Email,
|
||||||
displayName,
|
displayName,
|
||||||
@@ -135,14 +141,16 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo
|
|||||||
}
|
}
|
||||||
SendAsync(msgs...)
|
SendAsync(msgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo_model.Repository, run *actions_model.ActionRun) {
|
func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo_model.Repository, run *actions_model.ActionRun) error {
|
||||||
if setting.MailService == nil {
|
if setting.MailService == nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
if run.Status.IsSkipped() {
|
if !run.Status.IsDone() || run.Status.IsSkipped() {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
recipients := make([]*user_model.User, 0)
|
recipients := make([]*user_model.User, 0)
|
||||||
@@ -151,8 +159,7 @@ func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo
|
|||||||
notifyPref, err := user_model.GetUserSetting(ctx, sender.ID,
|
notifyPref, err := user_model.GetUserSetting(ctx, sender.ID,
|
||||||
user_model.SettingsKeyEmailNotificationGiteaActions, user_model.SettingEmailNotificationGiteaActionsFailureOnly)
|
user_model.SettingsKeyEmailNotificationGiteaActions, user_model.SettingEmailNotificationGiteaActionsFailureOnly)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUserSetting: %v", err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if notifyPref == user_model.SettingEmailNotificationGiteaActionsAll || !run.Status.IsSuccess() && notifyPref != user_model.SettingEmailNotificationGiteaActionsDisabled {
|
if notifyPref == user_model.SettingEmailNotificationGiteaActionsAll || !run.Status.IsSuccess() && notifyPref != user_model.SettingEmailNotificationGiteaActionsDisabled {
|
||||||
recipients = append(recipients, sender)
|
recipients = append(recipients, sender)
|
||||||
@@ -160,6 +167,8 @@ func MailActionsTrigger(ctx context.Context, sender *user_model.User, repo *repo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(recipients) > 0 {
|
if len(recipients) > 0 {
|
||||||
composeAndSendActionsWorkflowRunStatusEmail(ctx, repo, run, sender, recipients)
|
log.Debug("MailActionsTrigger: Initiate email composition")
|
||||||
|
return composeAndSendActionsWorkflowRunStatusEmail(ctx, repo, run, sender, recipients)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -208,8 +208,7 @@ 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) {
|
func (m *mailNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
|
||||||
if !run.Status.IsDone() {
|
if err := MailActionsTrigger(ctx, sender, repo, run); err != nil {
|
||||||
return
|
log.Error("MailActionsTrigger: %v", err)
|
||||||
}
|
}
|
||||||
MailActionsTrigger(ctx, sender, repo, run)
|
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/repo"
|
"code.gitea.io/gitea/models/repo"
|
||||||
@@ -24,7 +25,9 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
"code.gitea.io/gitea/services/actions"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
@@ -1102,45 +1105,465 @@ type workflowRunWebhook struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_WebhookWorkflowRun(t *testing.T) {
|
func Test_WebhookWorkflowRun(t *testing.T) {
|
||||||
webhookData := &workflowRunWebhook{}
|
testCases := []struct {
|
||||||
provider := newMockWebhookProvider(func(r *http.Request) {
|
|
||||||
assert.Contains(t, r.Header["X-Github-Event-Type"], "workflow_run", "X-GitHub-Event-Type should contain workflow_run")
|
|
||||||
assert.Contains(t, r.Header["X-Gitea-Event-Type"], "workflow_run", "X-Gitea-Event-Type should contain workflow_run")
|
|
||||||
assert.Contains(t, r.Header["X-Gogs-Event-Type"], "workflow_run", "X-Gogs-Event-Type should contain workflow_run")
|
|
||||||
content, _ := io.ReadAll(r.Body)
|
|
||||||
var payload api.WorkflowRunPayload
|
|
||||||
err := json.Unmarshal(content, &payload)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
webhookData.payloads = append(webhookData.payloads, payload)
|
|
||||||
webhookData.triggeredEvent = "workflow_run"
|
|
||||||
}, http.StatusOK)
|
|
||||||
defer provider.Close()
|
|
||||||
webhookData.URL = provider.URL()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
name string
|
||||||
callback func(t *testing.T, webhookData *workflowRunWebhook)
|
testFunc func(t *testing.T, webhookData *workflowRunWebhook)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "WorkflowRun",
|
name: "WorkflowRun",
|
||||||
callback: testWebhookWorkflowRun,
|
testFunc: testWebhookWorkflowRun,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "WorkflowRunDepthLimit",
|
name: "WorkflowRunDepthLimit",
|
||||||
callback: testWebhookWorkflowRunDepthLimit,
|
testFunc: testWebhookWorkflowRunDepthLimit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WorkflowRunEvents",
|
||||||
|
testFunc: testWorkflowRunEvents,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WorkflowRunEventsOnRerun",
|
||||||
|
testFunc: testWorkflowRunEventsOnRerun,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WorkflowRunEventsOnCancellingAbandonedRunAllJobsAbandoned",
|
||||||
|
testFunc: func(t *testing.T, webhookData *workflowRunWebhook) {
|
||||||
|
testWorkflowRunEventsOnCancellingAbandonedRun(t, webhookData, true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WorkflowRunEventsOnCancellingAbandonedRunPartiallyAbandoned",
|
||||||
|
testFunc: func(t *testing.T, webhookData *workflowRunWebhook) {
|
||||||
|
testWorkflowRunEventsOnCancellingAbandonedRun(t, webhookData, false)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, obj := range testCases {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(obj.name, func(t *testing.T) {
|
||||||
webhookData.payloads = nil
|
|
||||||
webhookData.triggeredEvent = ""
|
|
||||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||||
test.callback(t, webhookData)
|
webhookData := &workflowRunWebhook{}
|
||||||
|
provider := newMockWebhookProvider(func(r *http.Request) {
|
||||||
|
assert.Contains(t, r.Header["X-Github-Event-Type"], "workflow_run", "X-GitHub-Event-Type should contain workflow_run")
|
||||||
|
assert.Contains(t, r.Header["X-Gitea-Event-Type"], "workflow_run", "X-Gitea-Event-Type should contain workflow_run")
|
||||||
|
assert.Contains(t, r.Header["X-Gogs-Event-Type"], "workflow_run", "X-Gogs-Event-Type should contain workflow_run")
|
||||||
|
content, _ := io.ReadAll(r.Body)
|
||||||
|
var payload api.WorkflowRunPayload
|
||||||
|
err := json.Unmarshal(content, &payload)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
webhookData.payloads = append(webhookData.payloads, payload)
|
||||||
|
webhookData.triggeredEvent = "workflow_run"
|
||||||
|
}, http.StatusOK)
|
||||||
|
defer provider.Close()
|
||||||
|
webhookData.URL = provider.URL()
|
||||||
|
webhookData.payloads = nil
|
||||||
|
webhookData.triggeredEvent = ""
|
||||||
|
obj.testFunc(t, webhookData)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testWorkflowRunEvents(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)
|
||||||
|
|
||||||
|
// 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 testWorkflowRunEventsOnRerun(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)
|
||||||
|
|
||||||
|
runners := make([]*mockRunner, 2)
|
||||||
|
for i := range runners {
|
||||||
|
runners[i] = newMockRunner()
|
||||||
|
runners[i].registerAsRepoRunner(t, "user2", "repo1", fmt.Sprintf("mock-runner-%d", i), []string{"ubuntu-latest"}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
for _, runner := range runners {
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
runner.execTask(t, task, &mockTaskOutcome{
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Call rerun ui api
|
||||||
|
// Only a web UI API exists for cancelling workflow runs, so use the UI endpoint.
|
||||||
|
rerunURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/rerun", webhookData.payloads[0].WorkflowRun.RunNumber)
|
||||||
|
req = NewRequestWithValues(t, "POST", rerunURL, map[string]string{
|
||||||
|
"_csrf": GetUserCSRFToken(t, session),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
assert.Len(t, webhookData.payloads, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWorkflowRunEventsOnCancellingAbandonedRun(t *testing.T, webhookData *workflowRunWebhook, allJobsAbandoned bool) {
|
||||||
|
defer test.MockVariableValue(&setting.Actions.AbandonedJobTimeout, 0*time.Nanosecond)()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
repoName := "test-workflow-run-cancelling-abandoned-run"
|
||||||
|
testRepo := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: createActionsTestRepo(t, token, repoName, false).ID})
|
||||||
|
|
||||||
|
runners := make([]*mockRunner, 2)
|
||||||
|
for i := range runners {
|
||||||
|
runners[i] = newMockRunner()
|
||||||
|
runners[i].registerAsRepoRunner(t, "user2", repoName,
|
||||||
|
fmt.Sprintf("mock-runner-%d", i), []string{"ubuntu-latest"}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
testAPICreateWebhookForRepo(t, session, "user2", repoName, webhookData.URL, "workflow_run")
|
||||||
|
|
||||||
|
ctx := t.Context()
|
||||||
|
gitRepo, err := gitrepo.OpenRepository(ctx, testRepo)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 2.2 trigger the webhooks
|
||||||
|
|
||||||
|
// add workflow file to the repo
|
||||||
|
// init the workflow
|
||||||
|
wfilename := "push.yml"
|
||||||
|
wfTreePath := ".gitea/workflows/" + wfilename
|
||||||
|
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, testRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||||
|
createWorkflowFile(t, token, "user2", repoName, wfTreePath, opts)
|
||||||
|
|
||||||
|
commitID, err := gitRepo.GetBranchCommitID(testRepo.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, testRepo.DefaultBranch, webhookData.payloads[0].WorkflowRun.HeadBranch)
|
||||||
|
assert.Equal(t, commitID, webhookData.payloads[0].WorkflowRun.HeadSha)
|
||||||
|
assert.Equal(t, repoName, webhookData.payloads[0].Repo.Name)
|
||||||
|
assert.Equal(t, "user2/"+repoName, webhookData.payloads[0].Repo.FullName)
|
||||||
|
|
||||||
|
if !allJobsAbandoned {
|
||||||
|
for _, runner := range runners {
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
runner.execTask(t, task, &mockTaskOutcome{
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this sleep to ensure the func can find the tasks by timestamp.
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
err = actions.CancelAbandonedJobs(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, webhookData.payloads, 2)
|
||||||
|
assert.Equal(t, "completed", webhookData.payloads[1].Action)
|
||||||
|
assert.Equal(t, "completed", webhookData.payloads[1].WorkflowRun.Status)
|
||||||
|
assert.Equal(t, testRepo.DefaultBranch, webhookData.payloads[1].WorkflowRun.HeadBranch)
|
||||||
|
assert.Equal(t, commitID, webhookData.payloads[1].WorkflowRun.HeadSha)
|
||||||
|
assert.Equal(t, repoName, webhookData.payloads[1].Repo.Name)
|
||||||
|
assert.Equal(t, "user2/"+repoName, webhookData.payloads[1].Repo.FullName)
|
||||||
|
}
|
||||||
|
|
||||||
func testWebhookWorkflowRun(t *testing.T, webhookData *workflowRunWebhook) {
|
func testWebhookWorkflowRun(t *testing.T, webhookData *workflowRunWebhook) {
|
||||||
// 1. create a new webhook with special webhook for repo1
|
// 1. create a new webhook with special webhook for repo1
|
||||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
Reference in New Issue
Block a user