diff --git a/models/actions/main_test.go b/models/actions/main_test.go index 27916f29ac..2eb923d9d0 100644 --- a/models/actions/main_test.go +++ b/models/actions/main_test.go @@ -13,6 +13,7 @@ func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ FixtureFiles: []string{ "action_runner.yml", + "repository.yml", "action_runner_token.yml", }, }) diff --git a/models/actions/run.go b/models/actions/run.go index c1e04071a8..61159bc929 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -187,6 +187,7 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err // InsertRun inserts a run // The title will be cut off at 255 characters if it's longer than 255 characters. +// We don't have to send the ActionRunNowDone notification here because there are no runs that start in a not done status. func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { ctx, commiter, err := db.TxContext(ctx) if err != nil { @@ -272,6 +273,18 @@ func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) { return &run, nil } +// GetRunBefore returns the last run that completed a given timestamp (not inclusive). +func GetRunBefore(ctx context.Context, repoID int64, timestamp timeutil.TimeStamp) (*ActionRun, error) { + var run ActionRun + has, err := db.GetEngine(ctx).Where("repo_id=? AND stopped IS NOT NULL AND stopped 0 { sess.Cols(cols...) diff --git a/models/actions/run_job.go b/models/actions/run_job.go index fffbb6670b..1fadb4b7c7 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -101,7 +101,9 @@ func GetRunJobsByRunID(ctx context.Context, runID int64) ([]*ActionRunJob, error return jobs, nil } -func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) { +// All calls to UpdateRunJobWithoutNotification that change run.Status for any run from a not done status to a done status must call the ActionRunNowDone notification channel. +// Use the wrapper function UpdateRunJob instead. +func UpdateRunJobWithoutNotification(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) { e := db.GetEngine(ctx) sess := e.ID(job.ID) @@ -154,7 +156,8 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col if run.Stopped.IsZero() && run.Status.IsDone() { run.Stopped = timeutil.TimeStampNow() } - if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil { + // As the caller has to ensure the ActionRunNowDone notification is sent we can ignore doing so here. + if err := UpdateRunWithoutNotification(ctx, run, "status", "started", "stopped"); err != nil { return 0, fmt.Errorf("update run %d: %w", run.ID, err) } } diff --git a/models/actions/run_test.go b/models/actions/run_test.go new file mode 100644 index 0000000000..11b03022ff --- /dev/null +++ b/models/actions/run_test.go @@ -0,0 +1,96 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package actions + +import ( + "testing" + "time" + + "forgejo.org/models/db" + "forgejo.org/models/unittest" + "forgejo.org/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetRunBefore(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + // this repo is part of the test database requiring loading "repository.yml" in main_test.go + var repoID int64 = 1 + + workflowID := "test_workflow" + + // third completed run + time1, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00") + require.NoError(t, err) + timeutil.MockSet(time1) + run1 := ActionRun{ + ID: 1, + Index: 1, + RepoID: repoID, + Stopped: timeutil.TimeStampNow(), + WorkflowID: workflowID, + } + + // fourth completed run + time2, err := time.Parse(time.RFC3339, "2024-08-31T15:47:55+08:00") + require.NoError(t, err) + timeutil.MockSet(time2) + run2 := ActionRun{ + ID: 2, + Index: 2, + RepoID: repoID, + Stopped: timeutil.TimeStampNow(), + WorkflowID: workflowID, + } + + // second completed run + time3, err := time.Parse(time.RFC3339, "2024-07-31T15:47:54+08:00") + require.NoError(t, err) + timeutil.MockSet(time3) + run3 := ActionRun{ + ID: 3, + Index: 3, + RepoID: repoID, + Stopped: timeutil.TimeStampNow(), + WorkflowID: workflowID, + } + + // first completed run + time4, err := time.Parse(time.RFC3339, "2024-06-30T15:47:54+08:00") + require.NoError(t, err) + timeutil.MockSet(time4) + run4 := ActionRun{ + ID: 4, + Index: 4, + RepoID: repoID, + Stopped: timeutil.TimeStampNow(), + WorkflowID: workflowID, + } + require.NoError(t, db.Insert(db.DefaultContext, &run1)) + runBefore, err := GetRunBefore(db.DefaultContext, repoID, run1.Stopped) + // there is no run before run1 + require.Error(t, err) + require.Nil(t, runBefore) + + // now there is only run3 before run1 + require.NoError(t, db.Insert(db.DefaultContext, &run3)) + runBefore, err = GetRunBefore(db.DefaultContext, repoID, run1.Stopped) + require.NoError(t, err) + assert.Equal(t, run3.ID, runBefore.ID) + + // there still is only run3 before run1 + require.NoError(t, db.Insert(db.DefaultContext, &run2)) + runBefore, err = GetRunBefore(db.DefaultContext, repoID, run1.Stopped) + require.NoError(t, err) + assert.Equal(t, run3.ID, runBefore.ID) + + // run4 is further away from run1 + require.NoError(t, db.Insert(db.DefaultContext, &run4)) + runBefore, err = GetRunBefore(db.DefaultContext, repoID, run1.Stopped) + require.NoError(t, err) + assert.Equal(t, run3.ID, runBefore.ID) +} diff --git a/models/actions/task.go b/models/actions/task.go index c68b5b97ff..93369db7e8 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -311,7 +311,8 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask } job.TaskID = task.ID - if n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}); err != nil { + // We never have to send a notification here because the job is started with a not done status. + if n, err := UpdateRunJobWithoutNotification(ctx, job, builder.Eq{"task_id": 0}); err != nil { return nil, false, err } else if n != 1 { return nil, false, nil diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 563832297d..364bbf34a8 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -383,7 +383,7 @@ func Rerun(ctx *context_module.Context) { run.PreviousDuration = run.Duration() run.Started = 0 run.Stopped = 0 - if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil { + if err := actions_service.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return } @@ -436,7 +436,7 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou job.Stopped = 0 if err := db.WithTx(ctx, func(ctx context.Context) error { - _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped") + _, err := actions_service.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped") return err }); err != nil { return err @@ -512,7 +512,7 @@ func Cancel(ctx *context_module.Context) { if job.TaskID == 0 { job.Status = actions_model.StatusCancelled job.Stopped = timeutil.TimeStampNow() - n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") + n, err := actions_service.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") if err != nil { return err } @@ -549,13 +549,13 @@ func Approve(ctx *context_module.Context) { if err := db.WithTx(ctx, func(ctx context.Context) error { run.NeedApproval = false run.ApprovedBy = doer.ID - if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil { + if err := actions_service.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil { return err } for _, job := range jobs { if len(job.Needs) == 0 && job.Status.IsBlocked() { job.Status = actions_model.StatusWaiting - _, err := actions_model.UpdateRunJob(ctx, job, nil, "status") + _, err := actions_service.UpdateRunJob(ctx, job, nil, "status") if err != nil { return err } diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index f34c07fe5c..c36dda55b2 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -88,7 +88,7 @@ func CancelAbandonedJobs(ctx context.Context) error { job.Status = actions_model.StatusCancelled job.Stopped = now if err := db.WithTx(ctx, func(ctx context.Context) error { - _, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped") + _, err := UpdateRunJob(ctx, job, nil, "status", "stopped") return err }); err != nil { log.Warn("cancel abandoned job %v: %v", job.ID, err) diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index d4ca029d46..942c698e73 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -59,7 +59,7 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { for _, job := range jobs { if status, ok := updates[job.ID]; ok { job.Status = status - if n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil { + if n, err := UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil { return err } else if n != 1 { return fmt.Errorf("no affected for updating blocked job %v", job.ID) diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 2d3a1d2107..756738608b 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -5,7 +5,9 @@ package actions import ( "context" + "errors" + actions_model "forgejo.org/models/actions" issues_model "forgejo.org/models/issues" packages_model "forgejo.org/models/packages" perm_model "forgejo.org/models/perm" @@ -17,9 +19,12 @@ import ( "forgejo.org/modules/repository" "forgejo.org/modules/setting" api "forgejo.org/modules/structs" + "forgejo.org/modules/util" webhook_module "forgejo.org/modules/webhook" "forgejo.org/services/convert" notify_service "forgejo.org/services/notify" + + "xorm.io/builder" ) type actionsNotifier struct { @@ -775,3 +780,71 @@ func (n *actionsNotifier) MigrateRepository(ctx context.Context, doer, u *user_m Sender: convert.ToUser(ctx, doer, nil), }).Notify(ctx) } + +func sendActionRunNowDoneNotificationIfNeeded(ctx context.Context, oldRun, newRun *actions_model.ActionRun) error { + if !oldRun.Status.IsDone() && newRun.Status.IsDone() { + lastRun, err := actions_model.GetRunBefore(ctx, newRun.RepoID, newRun.Stopped) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } + // when no last run was found lastRun is nil + if lastRun != nil { + if err = lastRun.LoadAttributes(ctx); err != nil { + return err + } + } + if err = newRun.LoadAttributes(ctx); err != nil { + return err + } + notify_service.ActionRunNowDone(ctx, newRun, oldRun.Status, lastRun) + } + return nil +} + +// wrapper of UpdateRunWithoutNotification with a call to the ActionRunNowDone notification channel +func UpdateRun(ctx context.Context, run *actions_model.ActionRun, cols ...string) error { + // run.ID is the only thing that must be given + oldRun, err := actions_model.GetRunByID(ctx, run.ID) + if err != nil { + return err + } + + if err = actions_model.UpdateRunWithoutNotification(ctx, run, cols...); err != nil { + return err + } + + newRun, err := actions_model.GetRunByID(ctx, run.ID) + if err != nil { + return err + } + return sendActionRunNowDoneNotificationIfNeeded(ctx, oldRun, newRun) +} + +// wrapper of UpdateRunJobWithoutNotification with a call to the ActionRunNowDone notification channel +func UpdateRunJob(ctx context.Context, job *actions_model.ActionRunJob, cond builder.Cond, cols ...string) (int64, error) { + runID := job.RunID + if runID == 0 { + // job.ID is the only thing that must be given + // Don't overwrite job here, we'd loose the change we need to make. + oldJob, err := actions_model.GetRunJobByID(ctx, job.ID) + if err != nil { + return 0, err + } + runID = oldJob.RunID + } + oldRun, err := actions_model.GetRunByID(ctx, runID) + if err != nil { + return 0, err + } + + affected, err := actions_model.UpdateRunJobWithoutNotification(ctx, job, cond, cols...) + if err != nil { + return affected, err + } + + newRun, err := actions_model.GetRunByID(ctx, runID) + if err != nil { + return affected, err + } + return affected, sendActionRunNowDoneNotificationIfNeeded(ctx, oldRun, newRun) +} diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 8d41797a0e..9bfa7d3e1d 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -198,7 +198,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin job.Stopped = timeutil.TimeStampNow() // Update the job's status and stopped time in the database. - n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") + n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") if err != nil { return err } diff --git a/services/actions/task.go b/services/actions/task.go index 6d89afed45..4e885f69cc 100644 --- a/services/actions/task.go +++ b/services/actions/task.go @@ -128,7 +128,7 @@ func StopTask(ctx context.Context, taskID int64, status actions_model.Status) er now := timeutil.TimeStampNow() task.Status = status task.Stopped = now - if _, err := actions_model.UpdateRunJob(ctx, &actions_model.ActionRunJob{ + if _, err := UpdateRunJob(ctx, &actions_model.ActionRunJob{ ID: task.JobID, Status: task.Status, Stopped: task.Stopped, @@ -198,7 +198,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task if err := actions_model.UpdateTask(ctx, task, "status", "stopped"); err != nil { return nil, err } - if _, err := actions_model.UpdateRunJob(ctx, &actions_model.ActionRunJob{ + if _, err := UpdateRunJob(ctx, &actions_model.ActionRunJob{ ID: task.JobID, Status: task.Status, Stopped: task.Stopped, diff --git a/services/notify/notifier.go b/services/notify/notifier.go index 00f98942d9..4d88a7ab95 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -6,6 +6,7 @@ package notify import ( "context" + actions_model "forgejo.org/models/actions" issues_model "forgejo.org/models/issues" packages_model "forgejo.org/models/packages" repo_model "forgejo.org/models/repo" @@ -76,4 +77,6 @@ type Notifier interface { PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) + + ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) } diff --git a/services/notify/notify.go b/services/notify/notify.go index fb30dfb609..9c50f69059 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -6,6 +6,7 @@ package notify import ( "context" + actions_model "forgejo.org/models/actions" issues_model "forgejo.org/models/issues" packages_model "forgejo.org/models/packages" repo_model "forgejo.org/models/repo" @@ -374,3 +375,13 @@ func ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) { notifier.ChangeDefaultBranch(ctx, repo) } } + +// ActionRunNowDone notifies that the old status priorStatus with (priorStatus.isDone() == false) of an ActionRun changed to run.Status with (run.Status.isDone() == true) +// lastRun might be nil (e.g. when the run is the first for this workflow). It is the last run of the same workflow for the same repo. +// It can be used to figure out if a successful run follows a failed one. +// Both run and lastRun need their attributes loaded. +func ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) { + for _, notifier := range notifiers { + notifier.ActionRunNowDone(ctx, run, priorStatus, lastRun) + } +} diff --git a/services/notify/null.go b/services/notify/null.go index 7182e69abb..9c76e5cbd3 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -6,6 +6,7 @@ package notify import ( "context" + actions_model "forgejo.org/models/actions" issues_model "forgejo.org/models/issues" packages_model "forgejo.org/models/packages" repo_model "forgejo.org/models/repo" @@ -211,3 +212,7 @@ func (*NullNotifier) PackageDelete(ctx context.Context, doer *user_model.User, p // ChangeDefaultBranch places a place holder function func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) { } + +// ActionRunNowDone places a place holder function +func (*NullNotifier) ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) { +} diff --git a/tests/integration/actions_run_now_done_notification_test.go b/tests/integration/actions_run_now_done_notification_test.go new file mode 100644 index 0000000000..9a2e118701 --- /dev/null +++ b/tests/integration/actions_run_now_done_notification_test.go @@ -0,0 +1,176 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package integration + +import ( + "context" + "net/url" + "strings" + "testing" + "time" + + actions_model "forgejo.org/models/actions" + "forgejo.org/models/db" + unit_model "forgejo.org/models/unit" + "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + "forgejo.org/modules/gitrepo" + "forgejo.org/modules/setting" + actions_service "forgejo.org/services/actions" + notify_service "forgejo.org/services/notify" + files_service "forgejo.org/services/repository/files" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockNotifier struct { + notify_service.NullNotifier + testIdx int + t *testing.T + runID int64 + lastRunID int64 +} + +var _ notify_service.Notifier = &mockNotifier{} + +func (m *mockNotifier) ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) { + switch m.testIdx { + case 0: + // we accept the first id as okay and just check that the following ones make sense + m.runID = run.ID + assert.Equal(m.t, actions_model.StatusSuccess, run.Status) + assert.Equal(m.t, actions_model.StatusRunning, priorStatus) + assert.Nil(m.t, lastRun) + case 1: + assert.Equal(m.t, m.runID, run.ID) + assert.Equal(m.t, actions_model.StatusFailure, run.Status) + assert.Equal(m.t, actions_model.StatusRunning, priorStatus) + assert.Equal(m.t, m.lastRunID, lastRun.ID) + assert.Equal(m.t, actions_model.StatusSuccess, lastRun.Status) + case 2: + assert.Equal(m.t, m.runID, run.ID) + assert.Equal(m.t, actions_model.StatusCancelled, run.Status) + assert.Equal(m.t, actions_model.StatusRunning, priorStatus) + assert.Equal(m.t, m.lastRunID, lastRun.ID) + assert.Equal(m.t, actions_model.StatusFailure, lastRun.Status) + case 3: + assert.Equal(m.t, m.runID, run.ID) + assert.Equal(m.t, actions_model.StatusSuccess, run.Status) + assert.Equal(m.t, actions_model.StatusRunning, priorStatus) + assert.Equal(m.t, m.lastRunID, lastRun.ID) + assert.Equal(m.t, actions_model.StatusCancelled, lastRun.Status) + case 4: + assert.Equal(m.t, m.runID, run.ID) + assert.Equal(m.t, actions_model.StatusSuccess, run.Status) + assert.Equal(m.t, actions_model.StatusRunning, priorStatus) + assert.Equal(m.t, m.lastRunID, lastRun.ID) + assert.Equal(m.t, actions_model.StatusSuccess, lastRun.Status) + default: + assert.Fail(m.t, "too many notifications") + } + m.lastRunID = m.runID + m.runID++ + m.testIdx++ +} + +// ensure all tests have been run +func (m *mockNotifier) complete() { + assert.Equal(m.t, 5, m.testIdx) +} + +func TestActionNowDoneNotification(t *testing.T) { + if !setting.Database.Type.IsSQLite3() { + t.Skip() + } + + onGiteaRun(t, func(t *testing.T, u *url.URL) { + notifier := mockNotifier{t: t, testIdx: 0, lastRunID: -1, runID: -1} + notify_service.RegisterNotifier(¬ifier) + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // create the repo + repo, sha, f := tests.CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch", + []unit_model.Type{unit_model.TypeActions}, nil, + []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".forgejo/workflows/dispatch.yml", + ContentReader: strings.NewReader( + "name: test\n" + + "on: [workflow_dispatch]\n" + + "jobs:\n" + + " test:\n" + + " runs-on: ubuntu-latest\n" + + " steps:\n" + + " - run: echo helloworld\n", + ), + }, + }, + ) + defer f() + + gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) + require.NoError(t, err) + defer gitRepo.Close() + + workflow, err := actions_service.GetWorkflowFromCommit(gitRepo, "main", "dispatch.yml") + require.NoError(t, err) + assert.Equal(t, "refs/heads/main", workflow.Ref) + assert.Equal(t, sha, workflow.Commit.ID.String()) + + inputGetter := func(key string) string { + return "" + } + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}) + + // 0: first successful run + _, _, err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2) + require.NoError(t, err) + task := runner.fetchTask(t) + runner.succeedAtTask(t, task) + + // we can't differentiate different runs without a delay + time.Sleep(time.Millisecond * 2000) + + // 1: failed run + _, _, err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2) + require.NoError(t, err) + task = runner.fetchTask(t) + runner.failAtTask(t, task) + + // we can't differentiate different runs without a delay + time.Sleep(time.Millisecond * 2000) + + // 2: canceled run + _, _, err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2) + require.NoError(t, err) + task = runner.fetchTask(t) + require.NoError(t, actions_service.StopTask(db.DefaultContext, task.Id, actions_model.StatusCancelled)) + + // we can't differentiate different runs without a delay + time.Sleep(time.Millisecond * 2000) + + // 3: successful run after failure + _, _, err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2) + require.NoError(t, err) + task = runner.fetchTask(t) + runner.succeedAtTask(t, task) + + // we can't differentiate different runs without a delay + time.Sleep(time.Millisecond * 2000) + + // 4: successful run after success + _, _, err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2) + require.NoError(t, err) + task = runner.fetchTask(t) + runner.succeedAtTask(t, task) + + notifier.complete() + }) +} diff --git a/tests/integration/actions_runner_test.go b/tests/integration/actions_runner_test.go index 93f38db9ee..3f9e57c41f 100644 --- a/tests/integration/actions_runner_test.go +++ b/tests/integration/actions_runner_test.go @@ -160,3 +160,30 @@ func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTa require.NoError(t, err) assert.Equal(t, outcome.result, resp.Msg.State.Result) } + +// Simply pretend we're running the task and succeed at that. +// We're that great! +func (r *mockRunner) succeedAtTask(t *testing.T, task *runnerv1.Task) { + resp, err := r.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ + State: &runnerv1.TaskState{ + Id: task.Id, + Result: runnerv1.Result_RESULT_SUCCESS, + StoppedAt: timestamppb.Now(), + }, + })) + require.NoError(t, err) + assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, resp.Msg.State.Result) +} + +// Pretend we're running the task, do nothing and fail at that. +func (r *mockRunner) failAtTask(t *testing.T, task *runnerv1.Task) { + resp, err := r.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ + State: &runnerv1.TaskState{ + Id: task.Id, + Result: runnerv1.Result_RESULT_FAILURE, + StoppedAt: timestamppb.Now(), + }, + })) + require.NoError(t, err) + assert.Equal(t, runnerv1.Result_RESULT_FAILURE, resp.Msg.State.Result) +}