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:
Andreas Ahlenstorf 2026-06-18 00:58:33 +02:00 committed by Mathieu Fenniak
commit a0bee7b0b8
22 changed files with 864 additions and 9 deletions

View file

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

View file

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

View file

@ -0,0 +1,5 @@
- id: 73711
uuid: 1ed5b10d-a3f9-4530-b2fa-a590a1c2c8ea
name: repository-runner
owner_id: 2
repo_id: 62

View file

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

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,8 @@
previous_duration: 0
created: 1776263479
updated: 1776279265
priority: 127
prioritize: true
- id: 455630
title: Running workflow

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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