Actions Done Notification (#7491)
Some checks are pending
/ release (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions

This PR depends on https://codeberg.org/forgejo/forgejo/pulls/7510

This PR renames UpdateRunJob to UpdateRunJobWithoutNotification and UpdateRun to  UpdateRunWithoutNotification and implements wrapper functions that also call the new ActionRunNowDone notification when needed.

This PR can be reviewed commit-by-commit.

# Things to Test
- [x] GetRunBefore
- [ ] integration test for sendActionRunNowDoneNotificationIfNeeded, UpdateRun and UpdateRunJob

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [ ] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

Co-authored-by: nobody <nobody@example.com>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7491
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: christopher-besch <mail@chris-besch.com>
Co-committed-by: christopher-besch <mail@chris-besch.com>
This commit is contained in:
christopher-besch 2025-04-24 15:15:24 +00:00 committed by Earl Warren
parent 0f6176470f
commit 05273fa8d2
16 changed files with 425 additions and 14 deletions

View file

@ -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",
},
})

View file

@ -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<?", repoID, timestamp).OrderBy("stopped DESC").Limit(1).Get(&run)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("run before: %w", util.ErrNotExist)
}
return &run, nil
}
func GetLatestRunForBranchAndWorkflow(ctx context.Context, repoID int64, branch, workflowFile, event string) (*ActionRun, error) {
var run ActionRun
q := db.GetEngine(ctx).Where("repo_id=?", repoID).And("workflow_id=?", workflowFile)
@ -320,7 +333,9 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
// UpdateRun updates a run.
// It requires the inputted run has Version set.
// It will return error if the version is not matched (it means the run has been changed after loaded).
func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
// All calls to UpdateRunWithoutNotification that change run.Status from a not done status to a done status must call the ActionRunNowDone notification channel.
// Use the wrapper function UpdateRun instead.
func UpdateRunWithoutNotification(ctx context.Context, run *ActionRun, cols ...string) error {
sess := db.GetEngine(ctx).ID(run.ID)
if len(cols) > 0 {
sess.Cols(cols...)

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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,

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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) {
}

View file

@ -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(&notifier)
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()
})
}

View file

@ -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)
}