feat: add the actions CancelRun and ApproveRun helpers
CancelRun Cancels all the jobs of a given run. It is very similar to the less generic web/repo/actions/view.go with two differences: - It updates NeedApproval - The commit status are created within the transaction It is also very similar to cancelJobsForRun in services/actions/schedule_tasks.go Keeping those DRY would require a small refactor that does not feel necessary at this moment. ApproveRun Approves all the jobs of a given run.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
-
|
||||
id: 900
|
||||
title: "run running"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "test.yaml"
|
||||
index: 4
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "synchronized"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
concurrency_group: abc123
|
||||
-
|
||||
id: 800
|
||||
title: "run waiting because jobs need approval"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "test.yaml"
|
||||
index: 5
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "synchronized"
|
||||
is_fork_pull_request: 0
|
||||
status: 5 # waiting
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 1
|
||||
approved_by: 0
|
||||
concurrency_group: def435
|
||||
@@ -0,0 +1,42 @@
|
||||
-
|
||||
id: 10900
|
||||
run_id: 900
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: job_1
|
||||
attempt: 0
|
||||
job_id: job_1
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
runs_on: '["docker"]'
|
||||
started: 1683636528
|
||||
-
|
||||
id: 11900
|
||||
run_id: 900
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: job_2
|
||||
attempt: 0
|
||||
job_id: job_2
|
||||
task_id: 711900
|
||||
status: 6 # running
|
||||
runs_on: '["docker"]'
|
||||
started: 1683636528
|
||||
-
|
||||
id: 10800
|
||||
run_id: 800
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: job_1
|
||||
attempt: 0
|
||||
job_id: job_1
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
runs_on: '["docker"]'
|
||||
started: 1683636528
|
||||
@@ -0,0 +1,40 @@
|
||||
-
|
||||
id: 710900
|
||||
job_id: 10900
|
||||
attempt: 0
|
||||
runner_id: 1
|
||||
status: 1 # success
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: 8d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2a867e
|
||||
token_salt: jVuKnSPGgy
|
||||
token_last_eight: eeb1a71a
|
||||
log_filename: artifact-test2/2f/47.log
|
||||
log_in_storage: 1
|
||||
log_length: 707
|
||||
log_size: 90179
|
||||
log_expired: 0
|
||||
-
|
||||
id: 711900
|
||||
job_id: 11900
|
||||
attempt: 0
|
||||
runner_id: 1
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: 7d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2a867e
|
||||
token_salt: jVuKnSPGgy
|
||||
token_last_eight: eeb1a71a
|
||||
log_filename: artifact-test2/2f/47.log
|
||||
log_in_storage: 1
|
||||
log_length: 707
|
||||
log_size: 90179
|
||||
log_expired: 0
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/modules/timeutil"
|
||||
)
|
||||
|
||||
func CancelRun(ctx context.Context, run *actions_model.ActionRun) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, job := range jobs {
|
||||
status := job.Status
|
||||
if status.IsDone() {
|
||||
continue
|
||||
}
|
||||
if job.TaskID == 0 {
|
||||
job.Status = actions_model.StatusCancelled
|
||||
job.Stopped = timeutil.TimeStampNow()
|
||||
_, err := actions_model.UpdateRunJobWithoutNotification(ctx, job, nil, "status", "stopped")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if run.NeedApproval {
|
||||
if err := actions_model.UpdateRunApprovalByID(ctx, run.ID, actions_model.DoesNotNeedApproval, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
CreateCommitStatus(ctx, jobs...)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ApproveRun(ctx context.Context, run *actions_model.ActionRun, doerID int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if len(job.Needs) == 0 && job.Status.IsBlocked() {
|
||||
job.Status = actions_model.StatusWaiting
|
||||
_, err := UpdateRunJob(ctx, job, nil, "status")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
CreateCommitStatus(ctx, jobs...)
|
||||
|
||||
return actions_model.UpdateRunApprovalByID(ctx, run.ID, actions_model.DoesNotNeedApproval, doerID)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActions_CancelOrApproveRun(t *testing.T) {
|
||||
t.Run("run, job and task Running changes to run, job and task Cancelled", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestActions_CancelOrApproveRun")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
taskID := int64(711900)
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: taskID})
|
||||
require.Equal(t, actions_model.StatusRunning.String(), task.Status.String())
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID})
|
||||
require.Equal(t, actions_model.StatusRunning.String(), job.Status.String())
|
||||
require.Zero(t, job.Stopped)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
require.Equal(t, actions_model.StatusRunning.String(), run.Status.String())
|
||||
|
||||
require.NoError(t, CancelRun(t.Context(), run))
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID})
|
||||
assert.Equal(t, actions_model.StatusCancelled.String(), job.Status.String())
|
||||
assert.NotZero(t, job.Stopped)
|
||||
task = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: taskID})
|
||||
require.Equal(t, actions_model.StatusCancelled.String(), task.Status.String())
|
||||
})
|
||||
|
||||
t.Run("run Running, job and task Success changes to run Cancelled", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestActions_CancelOrApproveRun")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
taskID := int64(710900)
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: taskID})
|
||||
require.Equal(t, actions_model.StatusSuccess.String(), task.Status.String())
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID})
|
||||
require.Equal(t, actions_model.StatusSuccess.String(), job.Status.String())
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
require.Equal(t, actions_model.StatusRunning.String(), run.Status.String())
|
||||
|
||||
require.NoError(t, CancelRun(t.Context(), run))
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, job.Status)
|
||||
task = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: taskID})
|
||||
require.Equal(t, actions_model.StatusSuccess, task.Status)
|
||||
})
|
||||
|
||||
t.Run("run Waiting and job Blocked for Approval changes to run and job Cancelled", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestActions_CancelOrApproveRun")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
jobID := int64(10800)
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: jobID})
|
||||
require.Equal(t, actions_model.StatusBlocked.String(), job.Status.String())
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
require.Equal(t, actions_model.StatusWaiting.String(), run.Status.String())
|
||||
require.True(t, run.NeedApproval)
|
||||
|
||||
require.NoError(t, CancelRun(t.Context(), run))
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
|
||||
assert.False(t, run.NeedApproval)
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: jobID})
|
||||
assert.Equal(t, actions_model.StatusCancelled, job.Status)
|
||||
})
|
||||
|
||||
t.Run("run Waiting and job Blocked for Approval changes to job Waiting", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestActions_CancelOrApproveRun")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
jobID := int64(10800)
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: jobID})
|
||||
require.Equal(t, actions_model.StatusBlocked.String(), job.Status.String())
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
require.Equal(t, actions_model.StatusWaiting.String(), run.Status.String())
|
||||
require.True(t, run.NeedApproval)
|
||||
|
||||
doerID := int64(30)
|
||||
require.NoError(t, ApproveRun(t.Context(), run, doerID))
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
assert.Equal(t, actions_model.StatusWaiting.String(), run.Status.String())
|
||||
assert.False(t, run.NeedApproval)
|
||||
assert.Equal(t, doerID, run.ApprovedBy)
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: jobID})
|
||||
assert.Equal(t, actions_model.StatusWaiting, job.Status)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user