fix: don't abandon Action jobs waiting for approval (#11145)

On an open PR that is waiting for job approval, if jobs haven't been approved by the time the abandon timeout occurs they get marked as cancelled.  This doesn't match the expectations of abandoned jobs in my opinion, which is that they were never able to be dispatched to a runner (no matching labels), but these jobs never got a chance.  They should remain valid and blocked until approved.

Discovered while testing #11125, but unrelated to the behaviour fixed there.

## 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.
  - [ ] 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

- [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

*The decision if the pull request will be shown in the release notes is up to the mergers / release team.*

The content of the `release-notes/<pull request number>.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11145
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
Mathieu Fenniak 2026-02-04 16:00:18 +01:00 committed by Mathieu Fenniak
parent 13b7bdc741
commit 462ad7bb33
5 changed files with 119 additions and 8 deletions

View file

@ -8,6 +8,7 @@ import (
"forgejo.org/models/db"
"forgejo.org/modules/container"
"forgejo.org/modules/optional"
"forgejo.org/modules/timeutil"
"xorm.io/builder"
@ -48,12 +49,13 @@ func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) err
type FindRunJobOptions struct {
db.ListOptions
RunID int64
RepoID int64
OwnerID int64
CommitSHA string
Statuses []Status
UpdatedBefore timeutil.TimeStamp
RunID int64
RepoID int64
OwnerID int64
CommitSHA string
Statuses []Status
UpdatedBefore timeutil.TimeStamp
RunNeedsApproval optional.Option[bool]
}
func (opts FindRunJobOptions) ToConds() builder.Cond {
@ -76,5 +78,12 @@ func (opts FindRunJobOptions) ToConds() builder.Cond {
if opts.UpdatedBefore > 0 {
cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore})
}
if opts.RunNeedsApproval.Has() {
cond = cond.And(builder.Exists(builder.Select("id").From("action_run", "outer_run").
Where(builder.Eq{
"outer_run.need_approval": opts.RunNeedsApproval.Value(),
"outer_run.id": builder.Expr("run_id"),
})))
}
return cond
}

View file

@ -0,0 +1,25 @@
# Supporting data for Case 600 - 603
-
id: 900
title: "running"
owner_id: 2
repo_id: 63
workflow_id: "running.yaml"
index: 4
trigger_event: "push"
is_fork_pull_request: false
trigger_user_id: 2
need_approval: false
# Supporting data for Case 604
-
id: 901
title: "running"
owner_id: 2
repo_id: 63
workflow_id: "running.yaml"
index: 5
trigger_event: "push"
is_fork_pull_request: false
trigger_user_id: 2
need_approval: true

View file

@ -0,0 +1,34 @@
# Case 600 -- status waiting, too long, ready to be abandoned
-
id: 600
run_id: 900
status: 5 # waiting
updated: 981228409
# Case 601 -- status blocked, too long, ready to be abandoned
-
id: 601
run_id: 900
status: 7 # blocked
updated: 981228409
# Case 602 -- status blocked, *not* too long, not to be abandoned
-
id: 602
run_id: 900
status: 7 # blocked
updated: 4105366009 # set to 2100-02-03 -- sorry to whoever fixes this 75 years from now
# Case 603 -- status running, not to be abandoned
-
id: 603
run_id: 900
status: 6 # running
updated: 981228409
# Case 604 -- run needs approval (in action_run.yml), not to be abandoned
-
id: 604
run_id: 901
status: 5 # waiting
updated: 981228409

View file

@ -12,6 +12,7 @@ import (
"forgejo.org/models/db"
"forgejo.org/modules/actions"
"forgejo.org/modules/log"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting"
"forgejo.org/modules/timeutil"
)
@ -75,8 +76,9 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
// CancelAbandonedJobs cancels the jobs which have waiting status, but haven't been picked by a runner for a long time
func CancelAbandonedJobs(ctx context.Context) error {
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{
Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked},
UpdatedBefore: timeutil.TimeStamp(time.Now().Add(-setting.Actions.AbandonedJobTimeout).Unix()),
Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked},
UpdatedBefore: timeutil.TimeStamp(time.Now().Add(-setting.Actions.AbandonedJobTimeout).Unix()),
RunNeedsApproval: optional.Some(false),
})
if err != nil {
log.Warn("find abandoned tasks: %v", err)

View file

@ -0,0 +1,41 @@
// Copyright 2026 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 TestCancelAbandonedJobs(t *testing.T) {
defer unittest.OverrideFixtures("services/actions/TestCancelAbandonedJobs")()
require.NoError(t, unittest.PrepareTestDatabase())
require.NoError(t, CancelAbandonedJobs(t.Context()))
// status waiting, too long, ready to be abandoned
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 600})
assert.Equal(t, actions_model.StatusCancelled, job.Status)
// status blocked, too long, ready to be abandoned
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 601})
assert.Equal(t, actions_model.StatusCancelled, job.Status)
// status blocked, *not* too long, not to be abandoned
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 602})
assert.Equal(t, actions_model.StatusBlocked, job.Status)
// status running, not to be abandoned
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 603})
assert.Equal(t, actions_model.StatusRunning, job.Status)
// related run needs approval, not to be abandoned
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 604})
assert.Equal(t, actions_model.StatusWaiting, job.Status)
}