Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions models/actions/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ type ActionRunner struct {
AgentLabels []string `xorm:"TEXT"`
// Store if this is a runner that only ever get one single job assigned
Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
// Store if this runner is disabled and should not pick up new jobs
IsDisabled bool `xorm:"is_disabled NOT NULL DEFAULT false"`

Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
Expand Down Expand Up @@ -199,6 +201,7 @@ type FindRunnerOptions struct {
Sort string
Filter string
IsOnline optional.Option[bool]
IsDisabled optional.Option[bool]
WithAvailable bool // not only runners belong to, but also runners can be used
}

Expand Down Expand Up @@ -239,6 +242,10 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
}
}

if opts.IsDisabled.Has() {
cond = cond.And(builder.Eq{"is_disabled": opts.IsDisabled.Value()})
}
return cond
}

Expand Down Expand Up @@ -297,6 +304,20 @@ func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
return err
}

func SetRunnerDisabled(ctx context.Context, runner *ActionRunner, isDisabled bool) error {
if runner.IsDisabled == isDisabled {
return nil
}

return db.WithTx(ctx, func(ctx context.Context) error {
runner.IsDisabled = isDisabled
if err := UpdateRunner(ctx, runner, "is_disabled"); err != nil {
return err
}
return IncreaseTaskVersion(ctx, runner.OwnerID, runner.RepoID)
})
}

// DeleteRunner deletes a runner by given ID.
func DeleteRunner(ctx context.Context, id int64) error {
if _, err := GetRunnerByID(ctx, id); err != nil {
Expand Down
1 change: 1 addition & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ func prepareMigrationTasks() []*migration {
newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness),
newMigration(325, "Fix missed repo_id when migrate attachments", v1_26.FixMissedRepoIDWhenMigrateAttachments),
newMigration(326, "Migrate commit status target URL to use run ID and job ID", v1_26.FixCommitStatusTargetURLToUseRunAndJobID),
newMigration(327, "Add disabled state to action runners", v1_26.AddDisabledToActionRunner),
}
return preparedMigrations
}
Expand Down
17 changes: 17 additions & 0 deletions models/migrations/v1_26/v327.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_26

import "xorm.io/xorm"

func AddDisabledToActionRunner(x *xorm.Engine) error {
type ActionRunner struct {
IsDisabled bool `xorm:"is_disabled NOT NULL DEFAULT false"`
}

_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(ActionRunner))
return err
}
33 changes: 33 additions & 0 deletions models/migrations/v1_26/v327_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_26

import (
"testing"

"code.gitea.io/gitea/models/migrations/base"

"github.com/stretchr/testify/require"
)

func Test_AddDisabledToActionRunner(t *testing.T) {
type ActionRunner struct {
ID int64 `xorm:"pk autoincr"`
Name string
}

x, deferable := base.PrepareTestEnv(t, 0, new(ActionRunner))
defer deferable()

_, err := x.Insert(&ActionRunner{Name: "runner"})
require.NoError(t, err)

require.NoError(t, AddDisabledToActionRunner(x))

var isDisabled bool
has, err := x.SQL("SELECT is_disabled FROM action_runner WHERE id = ?", 1).Get(&isDisabled)
require.NoError(t, err)
require.True(t, has)
require.False(t, isDisabled)
}
8 changes: 8 additions & 0 deletions modules/structs/repo_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,18 @@ type ActionRunner struct {
Name string `json:"name"`
Status string `json:"status"`
Busy bool `json:"busy"`
Disabled bool `json:"disabled"`
Ephemeral bool `json:"ephemeral"`
Labels []*ActionRunnerLabel `json:"labels"`
}

// EditActionRunnerOption represents the editable fields for a runner.
// swagger:model
type EditActionRunnerOption struct {
// required: true
Disabled *bool `json:"disabled"`
}

// ActionRunnersResponse returns Runners
type ActionRunnersResponse struct {
Entries []*ActionRunner `json:"runners"`
Expand Down
7 changes: 7 additions & 0 deletions options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -3644,6 +3644,7 @@
"actions.runners.id": "ID",
"actions.runners.name": "Name",
"actions.runners.owner_type": "Type",
"actions.runners.availability": "Availability",
"actions.runners.description": "Description",
"actions.runners.labels": "Labels",
"actions.runners.last_online": "Last Online Time",
Expand All @@ -3659,6 +3660,12 @@
"actions.runners.update_runner": "Update Changes",
"actions.runners.update_runner_success": "Runner updated successfully",
"actions.runners.update_runner_failed": "Failed to update runner",
"actions.runners.enable_runner": "Enable this runner",
"actions.runners.enable_runner_success": "Runner enabled successfully",
"actions.runners.enable_runner_failed": "Failed to enable runner",
"actions.runners.disable_runner": "Disable this runner",
"actions.runners.disable_runner_success": "Runner disabled successfully",
"actions.runners.disable_runner_failed": "Failed to disable runner",
"actions.runners.delete_runner": "Delete this runner",
"actions.runners.delete_runner_success": "Runner deleted successfully",
"actions.runners.delete_runner_failed": "Failed to delete runner",
Expand Down
8 changes: 7 additions & 1 deletion routers/api/actions/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,16 @@ func (s *Service) FetchTask(
}

if tasksVersion != latestVersion {
// Re-load runner from DB so task assignment uses current IsDisabled state
// (avoids race where disable commits while this request still has stale runner).
freshRunner, err := actions_model.GetRunnerByUUID(ctx, runner.UUID)
if err != nil {
return nil, status.Errorf(codes.Internal, "get runner: %v", err)
}
// if the task version in request is not equal to the version in db,
// it means there may still be some tasks that haven't been assigned.
// try to pick a task for the runner that send the request.
if t, ok, err := actions_service.PickTask(ctx, runner); err != nil {
if t, ok, err := actions_service.PickTask(ctx, freshRunner); err != nil {
log.Error("pick task failed: %v", err)
return nil, status.Errorf(codes.Internal, "pick task: %v", err)
} else if ok {
Expand Down
37 changes: 37 additions & 0 deletions routers/api/v1/admin/runners.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ func ListRunners(ctx *context.APIContext) {
// summary: Get all runners
// produces:
// - application/json
// parameters:
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
Expand Down Expand Up @@ -87,3 +93,34 @@ func DeleteRunner(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}

// UpdateRunner update a global runner
func UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /admin/actions/runners/{runner_id} admin updateAdminRunner
// ---
// summary: Update a global runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
3 changes: 3 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,7 @@ func Routes() *web.Router {
m.Post("/registration-token", reqToken(), reqOwnerCheck, act.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), reqOwnerCheck, act.GetRunner)
m.Delete("/{runner_id}", reqToken(), reqOwnerCheck, act.DeleteRunner)
m.Patch("/{runner_id}", reqToken(), reqOwnerCheck, bind(api.EditActionRunnerOption{}), act.UpdateRunner)
})
m.Get("/runs", reqToken(), reqReaderCheck, act.ListWorkflowRuns)
m.Get("/jobs", reqToken(), reqReaderCheck, act.ListWorkflowJobs)
Expand Down Expand Up @@ -1043,6 +1044,7 @@ func Routes() *web.Router {
m.Post("/registration-token", reqToken(), user.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), user.GetRunner)
m.Delete("/{runner_id}", reqToken(), user.DeleteRunner)
m.Patch("/{runner_id}", reqToken(), bind(api.EditActionRunnerOption{}), user.UpdateRunner)
})

m.Get("/runs", reqToken(), user.ListWorkflowRuns)
Expand Down Expand Up @@ -1728,6 +1730,7 @@ func Routes() *web.Router {
m.Post("/registration-token", admin.CreateRegistrationToken)
m.Get("/{runner_id}", admin.GetRunner)
m.Delete("/{runner_id}", admin.DeleteRunner)
m.Patch("/{runner_id}", bind(api.EditActionRunnerOption{}), admin.UpdateRunner)
})
m.Get("/runs", admin.ListWorkflowRuns)
m.Get("/jobs", admin.ListWorkflowJobs)
Expand Down
41 changes: 41 additions & 0 deletions routers/api/v1/org/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,11 @@ func (Action) ListRunners(ctx *context.APIContext) {
// description: name of the organization
// type: string
// required: true
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
Expand Down Expand Up @@ -551,6 +556,42 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
}

// UpdateRunner update an org-level runner
func (Action) UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/actions/runners/{runner_id} organization updateOrgRunner
// ---
// summary: Update an org-level runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
}

func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/jobs organization getOrgWorkflowJobs
// ---
Expand Down
54 changes: 50 additions & 4 deletions routers/api/v1/repo/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,11 @@ func (Action) ListRunners(ctx *context.APIContext) {
// description: name of the repo
// type: string
// required: true
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
Expand All @@ -564,11 +569,11 @@ func (Action) ListRunners(ctx *context.APIContext) {
shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID)
}

// GetRunner get an repo-level runner
// GetRunner get a repo-level runner
func (Action) GetRunner(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner
// ---
// summary: Get an repo-level runner
// summary: Get a repo-level runner
// produces:
// - application/json
// parameters:
Expand Down Expand Up @@ -597,11 +602,11 @@ func (Action) GetRunner(ctx *context.APIContext) {
shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}

// DeleteRunner delete an repo-level runner
// DeleteRunner delete a repo-level runner
func (Action) DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner
// ---
// summary: Delete an repo-level runner
// summary: Delete a repo-level runner
// produces:
// - application/json
// parameters:
Expand Down Expand Up @@ -630,6 +635,47 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}

// UpdateRunner update a repo-level runner
func (Action) UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/actions/runners/{runner_id} repository updateRepoRunner
// ---
// summary: Update a repo-level runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}

// GetWorkflowRunJobs Lists all jobs for a workflow run.
func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs
Expand Down
Loading
Loading