mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-06-22 10:02:15 +00:00
feat: enable manual prioritization of workflow runs (#13045)
Introduce the capability to manually prioritize individual workflow runs. If possible, manually prioritized workflow runs will be run before all others. If multiple workflow runs have been prioritized manually, they will be run in their order of arrival, not in the order they have been prioritized manually. Workflow run prioritization is best-effort, no matter whether a workflow run has been (de-)prioritized manually or by a prioritization algorithm. That means that it usually has an effect, but it's not guaranteed to have one. Workflow run prioritization is performed by implementations of `RunPrioritizationStrategy`. Currently, only one implementation exists: first in, first out, with the option to manually mark individual workflow runs as prioritized. It is possible to add more strategies in the future and make them selectable in the user interface per repository. Implementations of `RunPrioritizationStrategy` can only influence the ordering of `ActionRunJob`s by altering the priority of the `ActionRun` they belong to. That is a conscious choice to reduce the risks of deadlocks or other potentially weird behaviour that would be hard to debug. The priority of `ActionRun`s that are already running is not recalculated for the same reason. The run priority cannot be observed by external systems because it is neither exposed in the HTTP API nor to webhook listeners. That limitation can be alleviated in future versions. See also https://codeberg.org/forgejo/forgejo/issues/12830 and https://code.forgejo.org/forgejo/forgejo-actions-feature-requests/issues/92. ### Tests for Go changes - 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 ran... - [x] `make pr-go` before pushing ### 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/13045 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Reviewed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
parent
5a82da94e9
commit
a0bee7b0b8
22 changed files with 864 additions and 9 deletions
|
|
@ -0,0 +1,29 @@
|
|||
- id: 50401
|
||||
title: .forgejo/workflows/test.yaml
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2
|
||||
workflow_id: test.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
status: 5 # waiting
|
||||
priority: 0
|
||||
prioritize: false
|
||||
|
||||
- id: 50402
|
||||
title: .forgejo/workflows/test.yaml
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2
|
||||
workflow_id: test.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
status: 5 # waiting
|
||||
priority: 1
|
||||
prioritize: false
|
||||
|
||||
- id: 50403
|
||||
title: .forgejo/workflows/test.yaml
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2
|
||||
workflow_id: test.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
status: 5 # waiting
|
||||
priority: 0
|
||||
prioritize: false
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
- id: 504010
|
||||
run_id: 50401
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2
|
||||
name: test
|
||||
attempt: 1
|
||||
handle: 405b52f4-0781-405e-9525-9ec38e4a6db0
|
||||
status: 5 # waiting
|
||||
task_id: 0
|
||||
|
||||
- id: 504020
|
||||
run_id: 50402
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2
|
||||
name: test
|
||||
attempt: 1
|
||||
handle: 9b09552d-3272-41b2-8ec3-48b541ee3667
|
||||
status: 5 # waiting
|
||||
task_id: 0
|
||||
|
||||
- id: 504030
|
||||
run_id: 50403
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2
|
||||
name: test
|
||||
attempt: 1
|
||||
handle: 16335297-1e66-4469-afb2-3911fab9ebd0
|
||||
status: 5 # waiting
|
||||
task_id: 0
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
- id: 73711
|
||||
uuid: 1ed5b10d-a3f9-4530-b2fa-a590a1c2c8ea
|
||||
name: repository-runner
|
||||
owner_id: 2
|
||||
repo_id: 62
|
||||
|
|
@ -83,6 +83,13 @@ type ActionRun struct {
|
|||
PreExecutionError string `xorm:"LONGTEXT"` // deprecated: replaced with PreExecutionErrorCode and PreExecutionErrorDetails for better i18n
|
||||
PreExecutionErrorCode PreExecutionError
|
||||
PreExecutionErrorDetails []any `xorm:"JSON LONGTEXT"`
|
||||
|
||||
// Priority defines the numerical order in which tasks should be processed (best effort). Tasks with the highest
|
||||
// numbers are processed first. The value range is between -128 and +127; 0 is the default value.
|
||||
Priority int8 `xorm:"NOT NULL DEFAULT 0"`
|
||||
// Prioritize signals whether a user has requested that this run should be prioritized (`true`). It is a separate
|
||||
// value so that it does not get lost when prioritization algorithms change the ActionRun's Priority.
|
||||
Prioritize bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -280,6 +287,8 @@ func (run *ActionRun) PrepareNextAttempt() error {
|
|||
run.Status = StatusWaiting
|
||||
run.Started = 0
|
||||
run.Stopped = 0
|
||||
run.Priority = DefaultRunPriority
|
||||
run.Prioritize = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -524,6 +533,21 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
|
|||
return run, nil
|
||||
}
|
||||
|
||||
// GetQueuedRunsByRepoID returns all workflow runs that belong to the given repository and whose status is either
|
||||
// StatusWaiting or StatusBlocked.
|
||||
func GetQueuedRunsByRepoID(ctx context.Context, repoID int64) ([]*ActionRun, error) {
|
||||
query := db.GetEngine(ctx).
|
||||
Where("repo_id=?", repoID).
|
||||
In("status", []Status{StatusWaiting, StatusBlocked}).
|
||||
Asc("id")
|
||||
|
||||
var runs []*ActionRun
|
||||
if err := query.Find(&runs); err != nil {
|
||||
return nil, fmt.Errorf("cannot get queued workflow runs of repository %d: %w", repoID, err)
|
||||
}
|
||||
return runs, nil
|
||||
}
|
||||
|
||||
// Error returned when ActionRun's optimistic concurrency control has indicated that the record has been updated in the
|
||||
// database by another session since it was loaded in-memory in this session.
|
||||
var ErrActionRunOutOfDate = errors.New("run has changed")
|
||||
|
|
|
|||
56
models/actions/run_priority.go
Normal file
56
models/actions/run_priority.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"forgejo.org/modules/container"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxRunPriority is the highest possible priority of an ActionRun.
|
||||
MaxRunPriority int8 = 127
|
||||
// DefaultRunPriority is the default priority assigned to ActionRun instances.
|
||||
DefaultRunPriority int8 = 0
|
||||
// MinRunPriority is the lowest possible priority of an ActionRun.
|
||||
MinRunPriority int8 = -128
|
||||
)
|
||||
|
||||
type RunPrioritizationStrategy interface {
|
||||
// PrioritizeRuns updates the priority of all ActionRun instances passed as argument. It returns a set containing
|
||||
// the IDs of all ActionRun instances whose priority was changed, or an error.
|
||||
//
|
||||
// It is the responsibility of each implementation to handle the ActionRun's Prioritized field appropriately.
|
||||
// Ignoring it is explicitly allowed.
|
||||
//
|
||||
// Forgejo sorts jobs by the ActionRun's priority followed by the time they were last updated and their ID, which
|
||||
// results in FIFO order. That behaviour cannot be influenced by implementations. It also means that they only have
|
||||
// to change an ActionRun's priority if FIFO order is not desired.
|
||||
//
|
||||
// PrioritizeRuns participates in an ongoing transaction. Implementations are free to query the database, but should
|
||||
// refrain from writing to it. Changes to any other aspect of the ActionRun besides its priority are discarded.
|
||||
PrioritizeRuns(runs []*ActionRun) (container.Set[int64], error)
|
||||
}
|
||||
|
||||
var _ RunPrioritizationStrategy = DefaultPrioritizationStrategy{}
|
||||
|
||||
// DefaultPrioritizationStrategy boosts the priority of manually prioritized jobs, but retains the default order
|
||||
// otherwise.
|
||||
type DefaultPrioritizationStrategy struct{}
|
||||
|
||||
func (s DefaultPrioritizationStrategy) PrioritizeRuns(runs []*ActionRun) (container.Set[int64], error) {
|
||||
changedRuns := container.SetOf[int64]()
|
||||
for _, run := range runs {
|
||||
oldPriority := run.Priority
|
||||
if run.Prioritize {
|
||||
run.Priority = MaxRunPriority
|
||||
} else {
|
||||
run.Priority = DefaultRunPriority
|
||||
}
|
||||
if run.Priority != oldPriority {
|
||||
changedRuns.Add(run.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return changedRuns, nil
|
||||
}
|
||||
34
models/actions/run_priority_test.go
Normal file
34
models/actions/run_priority_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDefaultPrioritizationStrategy(t *testing.T) {
|
||||
runs := []*ActionRun{
|
||||
{ID: 2, Priority: 89, Prioritize: true},
|
||||
{ID: 1, Priority: MinRunPriority},
|
||||
{ID: 5, Priority: DefaultRunPriority},
|
||||
{ID: 3, Priority: MaxRunPriority, Prioritize: true},
|
||||
}
|
||||
|
||||
strategy := DefaultPrioritizationStrategy{}
|
||||
changedRuns, err := strategy.PrioritizeRuns(runs)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, changedRuns, 2)
|
||||
assert.Contains(t, changedRuns, int64(1))
|
||||
assert.Contains(t, changedRuns, int64(2))
|
||||
|
||||
assert.Len(t, runs, 4)
|
||||
assert.Contains(t, runs, &ActionRun{ID: 1, Priority: DefaultRunPriority, Prioritize: false})
|
||||
assert.Contains(t, runs, &ActionRun{ID: 2, Priority: MaxRunPriority, Prioritize: true})
|
||||
assert.Contains(t, runs, &ActionRun{ID: 3, Priority: MaxRunPriority, Prioritize: true})
|
||||
assert.Contains(t, runs, &ActionRun{ID: 5, Priority: DefaultRunPriority, Prioritize: false})
|
||||
}
|
||||
|
|
@ -719,3 +719,30 @@ func TestGetRunByID(t *testing.T) {
|
|||
require.ErrorIs(t, err, util.ErrNotExist)
|
||||
assert.Nil(t, run)
|
||||
}
|
||||
|
||||
func TestGetQueuedRunsByRepoID(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
fixtures := []*ActionRun{
|
||||
{ID: 535681, Index: 1, RepoID: 62, OwnerID: 2, Status: StatusSuccess},
|
||||
{ID: 535682, Index: 2, RepoID: 62, OwnerID: 2, Status: StatusRunning},
|
||||
{ID: 535683, Index: 3, RepoID: 62, OwnerID: 2, Status: StatusWaiting},
|
||||
{ID: 535684, Index: 4, RepoID: 62, OwnerID: 2, Status: StatusBlocked},
|
||||
{ID: 535685, Index: 1, RepoID: 1, OwnerID: 2, Status: StatusBlocked},
|
||||
{ID: 535686, Index: 2, RepoID: 1, OwnerID: 2, Status: StatusCancelled},
|
||||
}
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
runs, err := GetQueuedRunsByRepoID(t.Context(), 62)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, runs, 2)
|
||||
assert.Equal(t, int64(535683), runs[0].ID)
|
||||
assert.Equal(t, int64(535684), runs[1].ID)
|
||||
|
||||
runs, err = GetQueuedRunsByRepoID(t.Context(), 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, runs, 1)
|
||||
assert.Equal(t, int64(535685), runs[0].ID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -340,7 +340,12 @@ func GetAvailableJobsForRunner(e db.Engine, runner *ActionRunner) ([]*ActionRunJ
|
|||
}
|
||||
|
||||
var jobs []*ActionRunJob
|
||||
if err := e.Where("task_id=? AND status=?", 0, StatusWaiting).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil {
|
||||
if err := e.
|
||||
Join("INNER", "action_run", "action_run_job.run_id=action_run.id").
|
||||
Where("task_id=? AND action_run_job.status=?", 0, StatusWaiting).And(jobCond).
|
||||
Desc("action_run.priority").
|
||||
Asc("action_run_job.updated", "action_run_job.id").
|
||||
Find(&jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jobs, nil
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package actions
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -76,3 +77,20 @@ func TestActionTask_GetTasksByRunnerRequestKey(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Empty(t, tasks)
|
||||
}
|
||||
|
||||
func TestActionTask_GetAvailableJobsForRunner(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("models/actions/TestActionTask_GetAvailableJobsForRunner")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
runner := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 73711})
|
||||
|
||||
t.Run("Priority takes precedence", func(t *testing.T) {
|
||||
jobs, err := GetAvailableJobsForRunner(db.GetEngine(t.Context()), runner)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, jobs, 3)
|
||||
assert.Equal(t, int64(504020), jobs[0].ID)
|
||||
assert.Equal(t, int64(504010), jobs[1].ID)
|
||||
assert.Equal(t, int64(504030), jobs[2].ID)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
24
models/forgejo_migrations/v16c_action_run_priority.go
Normal file
24
models/forgejo_migrations/v16c_action_run_priority.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package forgejo_migrations
|
||||
|
||||
import (
|
||||
"code.forgejo.org/xorm/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerMigration(&Migration{
|
||||
Description: "add priority action_run",
|
||||
Upgrade: addActionRunPriority,
|
||||
})
|
||||
}
|
||||
|
||||
func addActionRunPriority(x *xorm.Engine) error {
|
||||
type ActionRun struct {
|
||||
Priority int8 `xorm:"NOT NULL DEFAULT 0"`
|
||||
Prioritize bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRun))
|
||||
return err
|
||||
}
|
||||
|
|
@ -854,6 +854,12 @@
|
|||
"actions.runs.delete.button": "Delete run",
|
||||
"actions.runs.delete.error": "Could not delete the workflow run.",
|
||||
"actions.runs.delete.confirm_action": "Do you really want to delete this workflow run?",
|
||||
"actions.runs.prioritized_run": "Prioritized",
|
||||
"actions.runs.prioritize_run": "Prioritize run",
|
||||
"actions.runs.deprioritize_run": "Do not prioritize run",
|
||||
"actions.runs.options": "Options",
|
||||
"actions.runs.prioritization_success": "Workflow run #%d is now being prioritized.",
|
||||
"actions.runs.deprioritization_success": "Workflow run #%d will no longer be prioritized.",
|
||||
"actions.jobs.not_started": "not started",
|
||||
"actions.workflow.job_parsing_error": "Unable to parse jobs in workflow: %v",
|
||||
"actions.workflow.event_detection_error": "Unable to parse supported events in workflow: %v",
|
||||
|
|
|
|||
|
|
@ -908,3 +908,61 @@ func statusDiagnostics(status actions_model.Status, job *actions_model.ActionRun
|
|||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func PrioritizeRun(ctx *app_context.Context) { //nolint:dupl
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64("run"))
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = actions_service.PrioritizeRun(ctx, run); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
actor := ctx.FormInt64("actor")
|
||||
page := ctx.FormInt("page")
|
||||
status := ctx.FormInt("status")
|
||||
selectedWorkflow := url.QueryEscape(ctx.FormString("workflow"))
|
||||
|
||||
redirectURL := fmt.Sprintf("%s/actions?actor=%d&page=%d&status=%d&workflow=%s",
|
||||
ctx.Repo.RepoLink, actor, page, status, selectedWorkflow)
|
||||
|
||||
ctx.Flash.Success(ctx.Locale.Tr("actions.runs.prioritization_success", run.Index))
|
||||
ctx.Redirect(redirectURL)
|
||||
}
|
||||
|
||||
func DeprioritizeRun(ctx *app_context.Context) { //nolint:dupl
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64("run"))
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = actions_service.DeprioritizeRun(ctx, run); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
actor := ctx.FormInt64("actor")
|
||||
page := ctx.FormInt("page")
|
||||
status := ctx.FormInt("status")
|
||||
selectedWorkflow := url.QueryEscape(ctx.FormString("workflow"))
|
||||
|
||||
redirectURL := fmt.Sprintf("%s/actions?actor=%d&page=%d&status=%d&workflow=%s",
|
||||
ctx.Repo.RepoLink, actor, page, status, selectedWorkflow)
|
||||
|
||||
ctx.Flash.Success(ctx.Locale.Tr("actions.runs.deprioritization_success", run.Index))
|
||||
ctx.Redirect(redirectURL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1623,6 +1623,8 @@ func registerRoutes(m *web.Route) {
|
|||
m.Get("/artifacts/{artifact_name_or_id}", actions.ArtifactsDownloadView)
|
||||
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
|
||||
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
||||
m.Post("/prioritize", reqRepoActionsWriter, actions.PrioritizeRun)
|
||||
m.Post("/deprioritize", reqRepoActionsWriter, actions.DeprioritizeRun)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
previous_duration: 0
|
||||
created: 1776263479
|
||||
updated: 1776279265
|
||||
priority: 127
|
||||
prioritize: true
|
||||
|
||||
- id: 455630
|
||||
title: Running workflow
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@
|
|||
previous_duration: 0
|
||||
created: 1776331495
|
||||
updated: 1776331721
|
||||
priority: 127
|
||||
prioritize: true
|
||||
|
||||
- id: 455660
|
||||
title: Workflow with independent jobs
|
||||
|
|
@ -100,4 +102,5 @@
|
|||
previous_duration: 0
|
||||
created: 1776331495
|
||||
updated: 1776331721
|
||||
|
||||
priority: 127
|
||||
prioritize: true
|
||||
|
|
|
|||
|
|
@ -87,13 +87,19 @@ func RerunAllJobs(ctx context.Context, run *actions_model.ActionRun) ([]*actions
|
|||
run.Status = actions_model.StatusWaiting
|
||||
run.Started = 0
|
||||
run.Stopped = 0
|
||||
run.Priority = actions_model.DefaultRunPriority
|
||||
run.Prioritize = false
|
||||
|
||||
// The columns have to be specified here to work around a xorm quirk: It won't update columns that are set to
|
||||
// their zero value without AllCols().
|
||||
if err := UpdateRun(ctx, run, "status", "started", "stopped", "previous_duration"); err != nil {
|
||||
if err := UpdateRun(ctx, run, "status", "started", "stopped", "previous_duration", "priority", "prioritize"); err != nil {
|
||||
return fmt.Errorf("cannot update run %d: %w", run.ID, err)
|
||||
}
|
||||
|
||||
if err := recalculateRunPriorities(ctx, run.RepoID); err != nil {
|
||||
return fmt.Errorf("could not recalculate workflow run priorities of repository %d: %w", run.RepoID, err)
|
||||
}
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load jobs of run %d: %w", run.ID, err)
|
||||
|
|
@ -146,10 +152,18 @@ func RerunJob(ctx context.Context, job *actions_model.ActionRunJob) ([]*actions_
|
|||
job.Run.Status = actions_model.StatusWaiting
|
||||
job.Run.Started = 0
|
||||
job.Run.Stopped = 0
|
||||
job.Run.Priority = actions_model.DefaultRunPriority
|
||||
job.Run.Prioritize = false
|
||||
|
||||
if err := UpdateRun(ctx, job.Run, "previous_duration", "status", "started", "stopped"); err != nil {
|
||||
// The columns have to be specified here to work around a xorm quirk: It won't update columns that are set
|
||||
// to their zero value without AllCols().
|
||||
if err := UpdateRun(ctx, job.Run, "previous_duration", "status", "started", "stopped", "priority", "prioritize"); err != nil {
|
||||
return fmt.Errorf("unable to update run %d of job %d: %w", job.RunID, job.ID, err)
|
||||
}
|
||||
|
||||
if err := recalculateRunPriorities(ctx, job.RepoID); err != nil {
|
||||
return fmt.Errorf("could not recalculate workflow run priorities of repository %d: %w", job.RepoID, err)
|
||||
}
|
||||
}
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@
|
|||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/unit"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -57,6 +59,12 @@ func TestRerun_RerunAllJobs(t *testing.T) {
|
|||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunAllJobs")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
var recalculateRepoTarget *int64
|
||||
defer test.MockVariableValue(&recalculateRunPriorities, func(_ context.Context, repoID int64) error {
|
||||
recalculateRepoTarget = &repoID
|
||||
return nil
|
||||
})()
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455620})
|
||||
|
||||
unittest.AssertCount(t, &actions_model.ActionArtifact{RunID: run.ID}, 1)
|
||||
|
|
@ -67,12 +75,16 @@ func TestRerun_RerunAllJobs(t *testing.T) {
|
|||
rerunJobs, err := RerunAllJobs(t.Context(), run)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, &run.RepoID, recalculateRepoTarget)
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455620})
|
||||
|
||||
assert.Equal(t, actions_model.StatusWaiting, run.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Stopped)
|
||||
assert.Equal(t, 11*time.Second, run.PreviousDuration)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, run.Priority)
|
||||
assert.False(t, run.Prioritize)
|
||||
|
||||
assert.Len(t, rerunJobs, 2)
|
||||
assert.Equal(t, int64(683880), rerunJobs[0].ID)
|
||||
|
|
@ -170,6 +182,12 @@ func TestRerun_RerunJob(t *testing.T) {
|
|||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunJob")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
var recalculateRepoTarget *int64
|
||||
defer test.MockVariableValue(&recalculateRunPriorities, func(_ context.Context, repoID int64) error {
|
||||
recalculateRepoTarget = &repoID
|
||||
return nil
|
||||
})()
|
||||
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683910})
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
|
||||
|
|
@ -183,11 +201,11 @@ func TestRerun_RerunJob(t *testing.T) {
|
|||
}, 0)
|
||||
|
||||
rerunJobs, err := RerunJob(t.Context(), job)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, rerunJobs, 1)
|
||||
assert.Equal(t, job.ID, rerunJobs[0].ID)
|
||||
assert.Equal(t, &run.RepoID, recalculateRepoTarget)
|
||||
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683910})
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job.RunID})
|
||||
|
|
@ -201,6 +219,8 @@ func TestRerun_RerunJob(t *testing.T) {
|
|||
assert.Zero(t, run.Started)
|
||||
assert.Zero(t, run.Stopped)
|
||||
assert.Equal(t, 86*time.Second, run.PreviousDuration)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, run.Priority)
|
||||
assert.False(t, run.Prioritize)
|
||||
|
||||
unittest.AssertCount(t, &actions_model.ActionArtifact{
|
||||
RunID: job.RunID, Status: int64(actions_model.ArtifactStatusPendingDeletion),
|
||||
|
|
@ -406,5 +426,7 @@ func TestRerun_RerunJob(t *testing.T) {
|
|||
assert.Equal(t, timeutil.TimeStamp(1776331635), run.Started)
|
||||
assert.Zero(t, run.Stopped)
|
||||
assert.Zero(t, run.PreviousDuration)
|
||||
assert.Equal(t, actions_model.MaxRunPriority, run.Priority)
|
||||
assert.True(t, run.Prioritize)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package actions
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
|
@ -217,3 +218,85 @@ func DeleteRun(ctx context.Context, runID int64) error {
|
|||
return actions_model.DeleteRun(ctx, run.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// PrioritizeRun marks the workflow run identified by the given ID as prioritized and recalculates the priority of each
|
||||
// run in the queue.
|
||||
func PrioritizeRun(ctx context.Context, run *actions_model.ActionRun) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if run == nil {
|
||||
return errors.New("run is nil")
|
||||
}
|
||||
|
||||
if run.Prioritize {
|
||||
// Run is already prioritized. There is nothing left to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
run.Prioritize = true
|
||||
if err := actions_model.UpdateRunWithoutNotification(ctx, run, "prioritize"); err != nil {
|
||||
return fmt.Errorf("could not update workflow run %d to prioritize: %w", run.ID, err)
|
||||
}
|
||||
|
||||
if err := recalculateRunPriorities(ctx, run.RepoID); err != nil {
|
||||
return fmt.Errorf("could not recalculate workflow run priorities of repository %d: %w", run.RepoID, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeprioritizeRun removes the prioritized mark from a workflow run, if present, and recalculates the priority of each
|
||||
// run in the queue.
|
||||
func DeprioritizeRun(ctx context.Context, run *actions_model.ActionRun) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if run == nil {
|
||||
return errors.New("run is nil")
|
||||
}
|
||||
|
||||
if !run.Prioritize {
|
||||
// Run is already not prioritized. There is nothing left to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
run.Prioritize = false
|
||||
if err := actions_model.UpdateRunWithoutNotification(ctx, run, "prioritize"); err != nil {
|
||||
return fmt.Errorf("could not update workflow run %d to deprioritize: %w", run.ID, err)
|
||||
}
|
||||
|
||||
if err := recalculateRunPriorities(ctx, run.RepoID); err != nil {
|
||||
return fmt.Errorf("could not recalculate workflow run priorities of repository %d: %w", run.RepoID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// recalculateRunPriorities recalculates the priority of all queued workflow runs that belong to the given repository.
|
||||
var recalculateRunPriorities = func(ctx context.Context, repoID int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
runs, err := actions_model.GetQueuedRunsByRepoID(ctx, repoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read pending workflow runs of repository %d: %w", repoID, err)
|
||||
}
|
||||
|
||||
strategy := actions_model.DefaultPrioritizationStrategy{}
|
||||
updatedRuns, err := strategy.PrioritizeRuns(runs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prioritize pending workflow runs of repository %d: %w", repoID, err)
|
||||
}
|
||||
|
||||
for _, run := range runs {
|
||||
if !updatedRuns.Contains(run.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = actions_model.UpdateRunWithoutNotification(ctx, run, "priority"); err != nil {
|
||||
return fmt.Errorf("failed to update reprioritized workflow run %d: %w", run.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// In the future notify webhook listeners. Pass *all* runs, not only updated runs to provide listeners a
|
||||
// complete view.
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,3 +199,206 @@ func TestDeleteRun(t *testing.T) {
|
|||
}, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrioritizeRun(t *testing.T) {
|
||||
t.Run("Run prioritized", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
runOne := &actions_model.ActionRun{
|
||||
ID: 408911, Index: 1, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting,
|
||||
Priority: actions_model.DefaultRunPriority, Prioritize: false,
|
||||
}
|
||||
runTwo := &actions_model.ActionRun{
|
||||
ID: 408912, Index: 2, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting, Priority: 25,
|
||||
}
|
||||
unittest.AssertSuccessfulInsert(t, runOne, runTwo)
|
||||
|
||||
err := PrioritizeRun(t.Context(), runOne)
|
||||
require.NoError(t, err)
|
||||
|
||||
prioritizedRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runOne.ID})
|
||||
assert.True(t, prioritizedRun.Prioritize)
|
||||
assert.Equal(t, actions_model.MaxRunPriority, prioritizedRun.Priority)
|
||||
|
||||
// Verify that the priority of the unrelated run has been recalculated, too.
|
||||
waitingRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runTwo.ID})
|
||||
assert.False(t, waitingRun.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, waitingRun.Priority)
|
||||
})
|
||||
|
||||
t.Run("Nothing happens if run already prioritized", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
runOne := &actions_model.ActionRun{
|
||||
ID: 408911, Index: 1, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting,
|
||||
Priority: actions_model.MaxRunPriority, Prioritize: true,
|
||||
}
|
||||
runTwo := &actions_model.ActionRun{
|
||||
ID: 408912, Index: 2, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting, Priority: 25,
|
||||
}
|
||||
unittest.AssertSuccessfulInsert(t, runOne, runTwo)
|
||||
|
||||
err := PrioritizeRun(t.Context(), runOne)
|
||||
require.NoError(t, err)
|
||||
|
||||
prioritizedRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runOne.ID})
|
||||
assert.True(t, prioritizedRun.Prioritize)
|
||||
assert.Equal(t, actions_model.MaxRunPriority, prioritizedRun.Priority)
|
||||
|
||||
// Verify that the priority of the unrelated run not been recalculated.
|
||||
waitingRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runTwo.ID})
|
||||
assert.False(t, waitingRun.Prioritize)
|
||||
assert.Equal(t, int8(25), waitingRun.Priority)
|
||||
})
|
||||
|
||||
t.Run("Completed run can be prioritized", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testRun := &actions_model.ActionRun{
|
||||
ID: 808441,
|
||||
Index: 1,
|
||||
RepoID: 62,
|
||||
OwnerID: 2,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Priority: actions_model.DefaultRunPriority,
|
||||
Prioritize: false,
|
||||
}
|
||||
unittest.AssertSuccessfulInsert(t, testRun)
|
||||
|
||||
err := PrioritizeRun(t.Context(), testRun)
|
||||
require.NoError(t, err)
|
||||
|
||||
prioritizedRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: testRun.ID})
|
||||
assert.True(t, prioritizedRun.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, prioritizedRun.Priority) // Unchanged because run completed.
|
||||
})
|
||||
|
||||
t.Run("Error if run is nil", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
err := PrioritizeRun(t.Context(), nil)
|
||||
require.ErrorContains(t, err, "run is nil")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeprioritizeRun(t *testing.T) {
|
||||
t.Run("Run deprioritized", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
runOne := &actions_model.ActionRun{
|
||||
ID: 408911, Index: 1, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting,
|
||||
Priority: actions_model.MaxRunPriority, Prioritize: true,
|
||||
}
|
||||
runTwo := &actions_model.ActionRun{
|
||||
ID: 408912, Index: 2, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting, Priority: 25,
|
||||
}
|
||||
unittest.AssertSuccessfulInsert(t, runOne, runTwo)
|
||||
|
||||
err := DeprioritizeRun(t.Context(), runOne)
|
||||
require.NoError(t, err)
|
||||
|
||||
deprioritizedRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runOne.ID})
|
||||
assert.False(t, deprioritizedRun.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, deprioritizedRun.Priority)
|
||||
|
||||
// Verify that the priority of the unrelated run has been recalculated, too.
|
||||
waitingRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runTwo.ID})
|
||||
assert.False(t, waitingRun.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, waitingRun.Priority)
|
||||
})
|
||||
|
||||
t.Run("Nothing happens if run not prioritized", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
runOne := &actions_model.ActionRun{
|
||||
ID: 408911, Index: 1, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting,
|
||||
Priority: actions_model.DefaultRunPriority, Prioritize: false,
|
||||
}
|
||||
runTwo := &actions_model.ActionRun{
|
||||
ID: 408912, Index: 2, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting, Priority: 25,
|
||||
}
|
||||
unittest.AssertSuccessfulInsert(t, runOne, runTwo)
|
||||
|
||||
err := DeprioritizeRun(t.Context(), runOne)
|
||||
require.NoError(t, err)
|
||||
|
||||
deprioritizedRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runOne.ID})
|
||||
assert.False(t, deprioritizedRun.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, deprioritizedRun.Priority)
|
||||
|
||||
// Verify that the priority of the unrelated run has *not* been recalculated.
|
||||
waitingRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runTwo.ID})
|
||||
assert.False(t, waitingRun.Prioritize)
|
||||
assert.Equal(t, int8(25), waitingRun.Priority)
|
||||
})
|
||||
|
||||
t.Run("Completed run can be deprioritized", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testRun := &actions_model.ActionRun{
|
||||
ID: 535681,
|
||||
Index: 1,
|
||||
RepoID: 62,
|
||||
OwnerID: 2,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Priority: actions_model.MaxRunPriority,
|
||||
Prioritize: true,
|
||||
}
|
||||
unittest.AssertSuccessfulInsert(t, testRun)
|
||||
|
||||
err := DeprioritizeRun(t.Context(), testRun)
|
||||
require.NoError(t, err)
|
||||
|
||||
deprioritizedRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: testRun.ID})
|
||||
assert.False(t, deprioritizedRun.Prioritize)
|
||||
assert.Equal(t, actions_model.MaxRunPriority, deprioritizedRun.Priority) // Unchanged because run completed.
|
||||
})
|
||||
|
||||
t.Run("Error if run is nil", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
err := DeprioritizeRun(t.Context(), nil)
|
||||
require.ErrorContains(t, err, "run is nil")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecalculateRunPriorities(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
fixtures := []*actions_model.ActionRun{
|
||||
{ID: 535681, Index: 1, RepoID: 62, OwnerID: 2, Status: actions_model.StatusSuccess},
|
||||
{ID: 535682, Index: 2, RepoID: 62, OwnerID: 2, Status: actions_model.StatusRunning, Priority: actions_model.DefaultRunPriority, Prioritize: true},
|
||||
{ID: 535683, Index: 3, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting, Priority: actions_model.DefaultRunPriority, Prioritize: true},
|
||||
{ID: 535684, Index: 4, RepoID: 62, OwnerID: 2, Status: actions_model.StatusBlocked, Priority: actions_model.MaxRunPriority},
|
||||
{ID: 535685, Index: 1, RepoID: 1, OwnerID: 2, Status: actions_model.StatusBlocked, Priority: actions_model.DefaultRunPriority, Prioritize: true},
|
||||
{ID: 535686, Index: 5, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting},
|
||||
}
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
err := recalculateRunPriorities(t.Context(), 62)
|
||||
require.NoError(t, err)
|
||||
|
||||
runOne := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535681})
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runOne.Priority)
|
||||
assert.False(t, runOne.Prioritize)
|
||||
|
||||
runTwo := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535682})
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runTwo.Priority) // Unchanged because already running.
|
||||
assert.True(t, runTwo.Prioritize)
|
||||
|
||||
runThree := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535683})
|
||||
assert.Equal(t, actions_model.MaxRunPriority, runThree.Priority)
|
||||
assert.True(t, runThree.Prioritize)
|
||||
|
||||
runFour := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535684})
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runFour.Priority)
|
||||
assert.False(t, runFour.Prioritize)
|
||||
|
||||
runFive := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535685})
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runFive.Priority) // Unchanged because different repository.
|
||||
assert.True(t, runFive.Prioritize)
|
||||
|
||||
runSix := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535686})
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runSix.Priority)
|
||||
assert.False(t, runSix.Prioritize)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ function pollingOk() {
|
|||
//
|
||||
// Can't inline this into the `hx-trigger` above because using a left-brace ('[') breaks htmx's trigger parsing.
|
||||
function noActiveDropdowns() {
|
||||
if (document.querySelector('[aria-expanded=true]') !== null)
|
||||
if (document.querySelector('[aria-expanded=true]') !== null || document.querySelector('details[open]') !== null) {
|
||||
return false;
|
||||
}
|
||||
const dropdownForm = document.querySelector('#branch-dropdown-form');
|
||||
if (dropdownForm !== null && dropdownForm.checkVisibility())
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@
|
|||
{{template "repo/actions/status" (dict "status" .Status.String)}}
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<a class="flex-item-title" title="{{.Title}}" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
|
||||
{{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}}
|
||||
</a>
|
||||
<div class="flex-item-title">
|
||||
<a title="{{.Title}}" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
|
||||
{{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}}
|
||||
</a>
|
||||
{{if .Prioritize}}<span class="ui label">{{ctx.Locale.Tr "actions.runs.prioritized_run"}}</span>{{end}}
|
||||
</div>
|
||||
<div class="flex-item-body">
|
||||
<b>{{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}</b> -
|
||||
{{if .IsScheduledRun -}}
|
||||
|
|
@ -35,6 +38,27 @@
|
|||
<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}</div>
|
||||
<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{.Duration}}</div>
|
||||
</div>
|
||||
<details class="dropdown">
|
||||
<summary class="border">
|
||||
{{ctx.Locale.Tr "actions.runs.options"}} {{svg "octicon-triangle-down" 14}}
|
||||
</summary>
|
||||
<form id="de_prioritize_{{.Index}}" method="post">
|
||||
<input type="hidden" name="workflow" value="{{$.CurWorkflow}}">
|
||||
<input type="hidden" name="actor" value="{{$.CurActor}}">
|
||||
<input type="hidden" name="status" value="{{$.CurStatus}}">
|
||||
<input type="hidden" name="page" value="{{$.Page.Paginater.Current}}">
|
||||
</form>
|
||||
<div class="content">
|
||||
<ul>
|
||||
<li>
|
||||
<button form="de_prioritize_{{.Index}}" formaction="{{.Link}}/prioritize" {{if or (not ($.Permission.CanWrite $.UnitTypeActions)) .Prioritize .Status.IsDone}}disabled{{end}}>{{ctx.Locale.Tr "actions.runs.prioritize_run"}}</button>
|
||||
</li>
|
||||
<li>
|
||||
<button form="de_prioritize_{{.Index}}" formaction="{{.Link}}/deprioritize" {{if or (not ($.Permission.CanWrite $.UnitTypeActions)) (not .Prioritize) .Status.IsDone}}disabled{{end}}>{{ctx.Locale.Tr "actions.runs.deprioritize_run"}}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -121,3 +121,189 @@ func TestActionRunDeletion(t *testing.T) {
|
|||
unittest.AssertNotExistsBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionRunPrioritization(t *testing.T) {
|
||||
fixtures := []*actions_model.ActionRun{
|
||||
{ID: 535681, Index: 1, RepoID: 62, OwnerID: 2, Status: actions_model.StatusSuccess, Priority: actions_model.DefaultRunPriority},
|
||||
{ID: 535682, Index: 2, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting, Priority: actions_model.DefaultRunPriority},
|
||||
{ID: 535683, Index: 3, RepoID: 62, OwnerID: 2, Status: actions_model.StatusBlocked, Priority: actions_model.MaxRunPriority},
|
||||
{ID: 535684, Index: 1, RepoID: 1, OwnerID: 2, Status: actions_model.StatusBlocked, Priority: actions_model.DefaultRunPriority, Prioritize: true},
|
||||
{ID: 535685, Index: 4, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting},
|
||||
}
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
repo62 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 62, OwnerID: user2.ID})
|
||||
|
||||
user2Sess := loginUser(t, user2.Name)
|
||||
user5Sess := loginUser(t, user5.Name)
|
||||
|
||||
t.Run("Prioritize run", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
request := NewRequest(t, "POST", fmt.Sprintf("/%s/actions/runs/2/prioritize", repo62.FullName()))
|
||||
response := user2Sess.MakeRequest(t, request, http.StatusSeeOther)
|
||||
|
||||
assert.Equal(t, "/user2/test_workflows/actions?actor=0&page=0&status=0&workflow=",
|
||||
response.Header().Get("Location"))
|
||||
|
||||
runTwo := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535682})
|
||||
assert.True(t, runTwo.Prioritize)
|
||||
assert.Equal(t, actions_model.MaxRunPriority, runTwo.Priority)
|
||||
|
||||
// Verify that the rest of the queue has been reprioritized.
|
||||
runThree := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535683})
|
||||
assert.False(t, runThree.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runThree.Priority)
|
||||
|
||||
runFour := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535684})
|
||||
assert.True(t, runFour.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runFour.Priority) // No change because different repository.
|
||||
|
||||
runFive := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535685})
|
||||
assert.False(t, runFive.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runFive.Priority)
|
||||
})
|
||||
|
||||
t.Run("Prioritize completed run", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
request := NewRequest(t, "POST", fmt.Sprintf("/%s/actions/runs/1/prioritize", repo62.FullName()))
|
||||
user2Sess.MakeRequest(t, request, http.StatusSeeOther)
|
||||
|
||||
runOne := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535681})
|
||||
assert.True(t, runOne.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runOne.Priority) // No change because run has completed.
|
||||
})
|
||||
|
||||
t.Run("Requires write permissions", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
request := NewRequest(t, "POST", fmt.Sprintf("/%s/actions/runs/2/prioritize", repo62.FullName()))
|
||||
user5Sess.MakeRequest(t, request, http.StatusNotFound)
|
||||
|
||||
// The run should not have been changed because user5 has not the necessary permissions.
|
||||
runTwo := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535682})
|
||||
assert.False(t, runTwo.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runTwo.Priority)
|
||||
|
||||
request = NewRequest(t, "POST", fmt.Sprintf("/%s/actions/runs/2/prioritize", repo62.FullName()))
|
||||
user2Sess.MakeRequest(t, request, http.StatusSeeOther)
|
||||
|
||||
runTwo = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535682})
|
||||
assert.True(t, runTwo.Prioritize)
|
||||
assert.Equal(t, actions_model.MaxRunPriority, runTwo.Priority)
|
||||
})
|
||||
|
||||
t.Run("Redirect URL contains supplied parameters", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
request := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/actions/runs/2/prioritize", repo62.FullName()),
|
||||
map[string]string{"actor": "10", "page": "3", "status": "2", "workflow": "test.yaml"})
|
||||
response := user2Sess.MakeRequest(t, request, http.StatusSeeOther)
|
||||
|
||||
assert.Equal(t, "/user2/test_workflows/actions?actor=10&page=3&status=2&workflow=test.yaml",
|
||||
response.Header().Get("Location"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionRunDeprioritization(t *testing.T) {
|
||||
fixtures := []*actions_model.ActionRun{
|
||||
{ID: 535681, Index: 1, RepoID: 62, OwnerID: 2, Status: actions_model.StatusSuccess, Priority: actions_model.MaxRunPriority, Prioritize: true},
|
||||
{ID: 535682, Index: 2, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting, Priority: actions_model.MaxRunPriority, Prioritize: true},
|
||||
{ID: 535683, Index: 3, RepoID: 62, OwnerID: 2, Status: actions_model.StatusBlocked, Priority: actions_model.MaxRunPriority},
|
||||
{ID: 535684, Index: 1, RepoID: 1, OwnerID: 2, Status: actions_model.StatusBlocked, Priority: actions_model.DefaultRunPriority, Prioritize: true},
|
||||
{ID: 535685, Index: 4, RepoID: 62, OwnerID: 2, Status: actions_model.StatusWaiting},
|
||||
}
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
repo62 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 62, OwnerID: user2.ID})
|
||||
|
||||
user2Sess := loginUser(t, user2.Name)
|
||||
user5Sess := loginUser(t, user5.Name)
|
||||
|
||||
t.Run("Deprioritize run", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
request := NewRequest(t, "POST", fmt.Sprintf("/%s/actions/runs/2/deprioritize", repo62.FullName()))
|
||||
response := user2Sess.MakeRequest(t, request, http.StatusSeeOther)
|
||||
|
||||
assert.Equal(t, "/user2/test_workflows/actions?actor=0&page=0&status=0&workflow=",
|
||||
response.Header().Get("Location"))
|
||||
|
||||
runTwo := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535682})
|
||||
assert.False(t, runTwo.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runTwo.Priority)
|
||||
|
||||
// Verify that the rest of the queue has been reprioritized.
|
||||
runThree := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535683})
|
||||
assert.False(t, runThree.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runThree.Priority)
|
||||
|
||||
runFour := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535684})
|
||||
assert.True(t, runFour.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runFour.Priority) // No change because different repository.
|
||||
|
||||
runFive := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535685})
|
||||
assert.False(t, runFive.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runFive.Priority)
|
||||
})
|
||||
|
||||
t.Run("Deprioritize completed run", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
request := NewRequest(t, "POST", fmt.Sprintf("/%s/actions/runs/1/deprioritize", repo62.FullName()))
|
||||
user2Sess.MakeRequest(t, request, http.StatusSeeOther)
|
||||
|
||||
runOne := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535681})
|
||||
assert.False(t, runOne.Prioritize)
|
||||
assert.Equal(t, actions_model.MaxRunPriority, runOne.Priority) // No change because run has completed.
|
||||
})
|
||||
|
||||
t.Run("Requires write permissions", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
request := NewRequest(t, "POST", fmt.Sprintf("/%s/actions/runs/2/deprioritize", repo62.FullName()))
|
||||
user5Sess.MakeRequest(t, request, http.StatusNotFound)
|
||||
|
||||
// The run should not have been changed because user5 has not the necessary permissions.
|
||||
runTwo := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535682})
|
||||
assert.True(t, runTwo.Prioritize)
|
||||
assert.Equal(t, actions_model.MaxRunPriority, runTwo.Priority)
|
||||
|
||||
request = NewRequest(t, "POST", fmt.Sprintf("/%s/actions/runs/2/deprioritize", repo62.FullName()))
|
||||
user2Sess.MakeRequest(t, request, http.StatusSeeOther)
|
||||
|
||||
runTwo = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 535682})
|
||||
assert.False(t, runTwo.Prioritize)
|
||||
assert.Equal(t, actions_model.DefaultRunPriority, runTwo.Priority)
|
||||
})
|
||||
|
||||
t.Run("Redirect URL contains supplied parameters", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
unittest.AssertSuccessfulInsert(t, fixtures)
|
||||
|
||||
request := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/actions/runs/2/deprioritize", repo62.FullName()),
|
||||
map[string]string{"actor": "25", "page": "8", "status": "3", "workflow": "build.yaml"})
|
||||
response := user2Sess.MakeRequest(t, request, http.StatusSeeOther)
|
||||
|
||||
assert.Equal(t, "/user2/test_workflows/actions?actor=25&page=8&status=3&workflow=build.yaml",
|
||||
response.Header().Get("Location"))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue