mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
fix: in actions_service cancelJobsForRun is bugous use killRun instead
The cancelJobsForRun function is redundant with the killRun function and has bugs: - It does not use a transaction and may fail in a non-recoverable way - It does not update the commit status of the run - It does not set NeedRemoval to false if needed Remove the cancelJobsForRun function and use killRun instead (fixing forgejo/forgejo#12386). Both calls are covered by existing tests: - TestCancelPreviousJobs - TestCancelPreviousWithConcurrencyGroup A new integration test TestActionsPullRequestTrustPushCancel is added to verify that the NeedApproval field is set to false whenever a run is cancelled (fixing forgejo/forgejo#12350). Closes forgejo/forgejo#12350 Closes forgejo/forgejo#12386
This commit is contained in:
parent
d63724ceab
commit
b6af380324
4 changed files with 71 additions and 59 deletions
|
|
@ -347,7 +347,7 @@ func UpdateRunApprovalByID(ctx context.Context, id int64, approval ApprovalType,
|
|||
func GetRunsNotDoneByRepoIDAndPullRequestPosterID(ctx context.Context, repoID, pullRequestPosterID int64) ([]*ActionRun, error) {
|
||||
var runs []*ActionRun
|
||||
// performance relies on indexes on repo_id and status
|
||||
if err := db.GetEngine(ctx).Where("repo_id=? AND pull_request_poster_id=?", repoID, pullRequestPosterID).And(builder.In("status", []Status{StatusUnknown, StatusWaiting, StatusRunning, StatusBlocked})).Find(&runs); err != nil {
|
||||
if err := db.GetEngine(ctx).Where("repo_id=? AND pull_request_poster_id=?", repoID, pullRequestPosterID).And(builder.In("status", PendingStatuses())).Find(&runs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return runs, nil
|
||||
|
|
@ -356,7 +356,7 @@ func GetRunsNotDoneByRepoIDAndPullRequestPosterID(ctx context.Context, repoID, p
|
|||
func GetRunsNotDoneByRepoIDAndPullRequestID(ctx context.Context, repoID, pullRequestID int64) ([]*ActionRun, error) {
|
||||
var runs []*ActionRun
|
||||
// performance relies on indexes on repo_id and status
|
||||
if err := db.GetEngine(ctx).Where("repo_id=? AND pull_request_id=?", repoID, pullRequestID).And(builder.In("status", []Status{StatusUnknown, StatusWaiting, StatusRunning, StatusBlocked})).Find(&runs); err != nil {
|
||||
if err := db.GetEngine(ctx).Where("repo_id=? AND pull_request_id=?", repoID, pullRequestID).And(builder.In("status", PendingStatuses())).Find(&runs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return runs, nil
|
||||
|
|
|
|||
1
release-notes/12366.md
Normal file
1
release-notes/12366.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
When the author of a pull request is [denied the right to run Actions](https://forgejo.org/docs/next/user/actions/security-pull-request/) by clicking on the "Deny" button on the pull request trust management panel, the workflow runs created for all commits pushed to the pull request are cancelled. Before that, runs that were automatically cancelled because a newer commit was pushed to the pull request [were stuck in a state waiting for approval](https://codeberg.org/forgejo/forgejo/issues/12350).
|
||||
|
|
@ -23,7 +23,6 @@ import (
|
|||
"code.forgejo.org/forgejo/runner/v12/act/jobparser"
|
||||
act_model "code.forgejo.org/forgejo/runner/v12/act/model"
|
||||
"github.com/gdgvda/cron"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// StartScheduleTasks start the task
|
||||
|
|
@ -220,7 +219,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
|||
// Iterate over each found run and cancel its associated jobs.
|
||||
errorSlice := []error{}
|
||||
for _, run := range runs {
|
||||
err := cancelJobsForRun(ctx, run.ID)
|
||||
err := killRun(ctx, run, actions_model.StatusCancelled)
|
||||
errorSlice = append(errorSlice, err)
|
||||
}
|
||||
err = errors.Join(errorSlice...)
|
||||
|
|
@ -236,21 +235,21 @@ func CancelPreviousWithConcurrencyGroup(ctx context.Context, repoID int64, concu
|
|||
// Find all runs in the concurrency group which have at least one job that is still pending; we can't use the run's
|
||||
// status for this because runs are set to failed if a single job is marked as failed, even if other jobs are still
|
||||
// running.
|
||||
runIDs := make([]int64, 0, 10)
|
||||
runs := make([]*actions_model.ActionRun, 0, 10)
|
||||
if err := db.GetEngine(ctx).Table("action_run").
|
||||
Join("INNER", "action_run_job", "action_run_job.run_id = action_run.id").
|
||||
Where("action_run.repo_id = ? AND action_run.concurrency_group = ?", repoID, strings.ToLower(concurrencyGroup)).
|
||||
In("action_run_job.status", actions_model.PendingStatuses()).
|
||||
Distinct("action_run.id").
|
||||
Select("action_run.id").
|
||||
Find(&runIDs); err != nil {
|
||||
Select("action_run.id,action_run.need_approval").
|
||||
Find(&runs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate over each found run and cancel its associated jobs.
|
||||
errorSlice := []error{}
|
||||
for _, runID := range runIDs {
|
||||
err := cancelJobsForRun(ctx, runID)
|
||||
for _, run := range runs {
|
||||
err := killRun(ctx, run, actions_model.StatusCancelled)
|
||||
errorSlice = append(errorSlice, err)
|
||||
}
|
||||
err := errors.Join(errorSlice...)
|
||||
|
|
@ -261,56 +260,6 @@ func CancelPreviousWithConcurrencyGroup(ctx context.Context, repoID int64, concu
|
|||
return nil
|
||||
}
|
||||
|
||||
func cancelJobsForRun(ctx context.Context, runID int64) error {
|
||||
// Find all jobs associated with the current run.
|
||||
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{
|
||||
RunID: runID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate over each job and attempt to cancel it.
|
||||
errorSlice := []error{}
|
||||
for _, job := range jobs {
|
||||
// Skip jobs that are already in a terminal state (completed, cancelled, etc.).
|
||||
status := job.Status
|
||||
if status.IsDone() {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it.
|
||||
if job.TaskID == 0 {
|
||||
job.Status = actions_model.StatusCancelled
|
||||
job.Stopped = timeutil.TimeStampNow()
|
||||
|
||||
// Update the job's status and stopped time in the database.
|
||||
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
|
||||
if err != nil {
|
||||
errorSlice = append(errorSlice, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
|
||||
if n == 0 {
|
||||
errorSlice = append(errorSlice, errors.New("job has changed, try again"))
|
||||
continue
|
||||
}
|
||||
|
||||
// Continue with the next job.
|
||||
continue
|
||||
}
|
||||
|
||||
// If the job has an associated task, try to stop the task, effectively cancelling the job.
|
||||
if err := StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
|
||||
errorSlice = append(errorSlice, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errorSlice...)
|
||||
}
|
||||
|
||||
func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository, cancelPreviousJobs bool) error {
|
||||
// If actions disabled when there is schedule task, this will remove the outdated schedule tasks
|
||||
// There is no other place we can do this because the app.ini will be changed manually
|
||||
|
|
|
|||
|
|
@ -471,3 +471,65 @@ func TestActionsPullRequestTrustCancelOnClose(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionsPullRequestTrustPushCancel(t *testing.T) {
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
ownerUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
ownerSession := loginUser(t, ownerUser.Name)
|
||||
|
||||
regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
|
||||
baseRepo, f := actionsTrustTestCreateBaseRepo(t, ownerUser)
|
||||
defer f()
|
||||
|
||||
forkedRepo, pullRequest, addFileToForkedResp := actionsTrustTestCreatePullRequestFromForkedRepo(t, ownerUser, baseRepo, regularUser)
|
||||
pullRequestLink := pullRequest.Issue.Link()
|
||||
|
||||
// there is one commit in the pull request and it is blocked from running actions pending approval
|
||||
{
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: addFileToForkedResp.Commit.SHA})
|
||||
assert.True(t, actionRun.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusWaiting, actionRun.Status)
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusBlocked, actionRunJob.Status)
|
||||
}
|
||||
|
||||
// another commit is pushed to the head branch of the pull request
|
||||
otherFileInForkResp := actionsTrustTestRepoModify(t, regularUser, baseRepo, forkedRepo, "add_file_one.txt")
|
||||
|
||||
// there are two commits
|
||||
// - the oldest one switched from Blocked to Cancelled and no longer needs approval
|
||||
// - the newest one is Blocked and pending approval
|
||||
{
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: addFileToForkedResp.Commit.SHA})
|
||||
assert.False(t, actionRun.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusCancelled, actionRun.Status)
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled, actionRunJob.Status)
|
||||
|
||||
otherActionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: otherFileInForkResp.Commit.SHA})
|
||||
assert.True(t, otherActionRun.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusWaiting, otherActionRun.Status)
|
||||
otherActionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: otherActionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusBlocked, otherActionRunJob.Status)
|
||||
}
|
||||
|
||||
actionsTrustTestAssertTrustPanel(t, ownerSession, pullRequestLink)
|
||||
actionsTrustTestClickTrustPanel(t, ownerSession, pullRequestLink, string(actions_service.UserTrustDenied))
|
||||
|
||||
// there are two commits, both are Cancelled and not pending approval
|
||||
{
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: addFileToForkedResp.Commit.SHA})
|
||||
assert.False(t, actionRun.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusCancelled, actionRun.Status)
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled, actionRunJob.Status)
|
||||
|
||||
otherActionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, CommitSHA: otherFileInForkResp.Commit.SHA})
|
||||
assert.False(t, otherActionRun.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusCancelled, otherActionRun.Status)
|
||||
otherActionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: otherActionRun.ID, RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled, otherActionRunJob.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue