diff --git a/models/actions/TestRunner_GetAvailableRunnerByID/action_runner.yml b/models/actions/TestRunner_GetAvailableRunnerByID/action_runner.yml
new file mode 100644
index 0000000000..1b8198236e
--- /dev/null
+++ b/models/actions/TestRunner_GetAvailableRunnerByID/action_runner.yml
@@ -0,0 +1,36 @@
+- id: 719931
+ uuid: "9e0eb762-cbdf-4725-9c90-4298568a2a77"
+ name: "runner-1"
+ version: "dev"
+ owner_id: 3 # Owned by org3
+ repo_id: 0
+ description: "A superb runner"
+ agent_labels: ["debian", "gpu"]
+ deleted: 0
+- id: 719932
+ uuid: "b0a0a168-1b08-4365-95dd-e65040ca384d"
+ name: "runner-2"
+ version: "11.3.1"
+ owner_id: 2 # Owned by user2
+ repo_id: 0
+ description: "An exclusive runner"
+ agent_labels: ["docker"]
+ deleted: 0
+- id: 719933
+ uuid: "974f9caf-ee64-4022-a56b-67b28ea06a0d"
+ name: "runner-3"
+ version: "12.2.0"
+ owner_id: 0
+ repo_id: 0
+ description: "A runner for everyone"
+ agent_labels: ["docker"]
+ deleted: 0
+- id: 719934
+ uuid: "022650a8-e999-4a3d-afb0-21f75233798b"
+ name: "runner-4"
+ version: "12.1.0"
+ owner_id: 0
+ repo_id: 32
+ description: ""
+ agent_labels: ["debian"]
+ deleted: 0
diff --git a/models/actions/runner.go b/models/actions/runner.go
index 3761cb6ccf..ea2e811f94 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -127,6 +127,18 @@ func (r *ActionRunner) IsOnline() bool {
return false
}
+func (r *ActionRunner) IsActive() bool {
+ return r.Status() == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE
+}
+
+func (r *ActionRunner) IsIdle() bool {
+ return r.Status() == runnerv1.RunnerStatus_RUNNER_STATUS_IDLE
+}
+
+func (r *ActionRunner) IsOffline() bool {
+ return r.Status() == runnerv1.RunnerStatus_RUNNER_STATUS_OFFLINE
+}
+
// Editable checks if the runner is editable by the user
func (r *ActionRunner) Editable(ownerID, repoID int64) bool {
if ownerID == 0 && repoID == 0 {
@@ -266,6 +278,31 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) {
return &runner, nil
}
+// GetAvailableRunnerByID is like GetRunnerByID, but it only finds the runner if it is accessible to the given owner or
+// repository. If it is not, util.ErrNotExist will be returned even if the runner exists.
+func GetAvailableRunnerByID(ctx context.Context, id, ownerID, repoID int64) (*ActionRunner, error) {
+ query := db.GetEngine(ctx).Where("id=?", id)
+
+ if repoID > 0 {
+ cond := builder.NewCond().And(builder.Eq{"repo_id": repoID})
+ cond = cond.Or(builder.Eq{"owner_id": builder.Select("owner_id").From("repository").Where(builder.Eq{"id": repoID})})
+ cond = cond.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
+ query = query.And(cond)
+ } else if ownerID > 0 { // ownerID is ignored if repoID is set
+ cond := builder.NewCond().And(builder.Eq{"owner_id": ownerID}).Or(builder.Eq{"repo_id": 0, "owner_id": 0})
+ query = query.And(cond)
+ }
+
+ var runner ActionRunner
+ has, err := query.Get(&runner)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, fmt.Errorf("runner with ID %d: %w", id, util.ErrNotExist)
+ }
+ return &runner, nil
+}
+
// UpdateRunner updates runner's information.
func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
e := db.GetEngine(ctx)
diff --git a/models/actions/runner_test.go b/models/actions/runner_test.go
index f6d6bd5d23..d279c776c6 100644
--- a/models/actions/runner_test.go
+++ b/models/actions/runner_test.go
@@ -10,6 +10,7 @@ import (
auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
+ "forgejo.org/models/repo"
"forgejo.org/models/unittest"
"forgejo.org/modules/timeutil"
@@ -235,3 +236,141 @@ func TestRunnerEditable(t *testing.T) {
})
}
}
+
+func TestRunner_GetAvailableRunnerByID(t *testing.T) {
+ defer unittest.OverrideFixtures("models/actions/TestRunner_GetAvailableRunnerByID")()
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ repository32 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 32, OwnerID: 3})
+ repository1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1, OwnerID: 2})
+
+ runner1 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719931, OwnerID: 3, RepoID: 0}) // Owned by org3
+ runner2 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719932, OwnerID: 2, RepoID: 0}) // Owned by user2
+ runner3 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719933, OwnerID: 0, RepoID: 0})
+ runner4 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719934, OwnerID: 0, RepoID: repository32.ID})
+
+ testCases := []struct {
+ name string
+ runner *ActionRunner
+ ownerID int64
+ repoID int64
+ expectedError string
+ }{
+ {
+ name: "Organization runner",
+ runner: runner1,
+ ownerID: 3,
+ repoID: 0,
+ expectedError: "",
+ },
+ {
+ name: "Organization runner visible to admins",
+ runner: runner1,
+ ownerID: 0,
+ repoID: 0,
+ expectedError: "",
+ },
+ {
+ name: "Organization runner invisible to different owner",
+ runner: runner1,
+ ownerID: 2,
+ repoID: 0,
+ expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner1.ID),
+ },
+ {
+ name: "Organization runner visible to its repositories",
+ runner: runner1,
+ ownerID: 0,
+ repoID: repository32.ID,
+ expectedError: "",
+ },
+ {
+ name: "Organization runner invisible to repositories owned by somebody else",
+ runner: runner1,
+ ownerID: 0,
+ repoID: repository1.ID,
+ expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner1.ID),
+ },
+ {
+ name: "User runner",
+ runner: runner2,
+ ownerID: 2,
+ repoID: 0,
+ expectedError: "",
+ },
+ {
+ name: "User runner invisible to different user",
+ runner: runner2,
+ ownerID: 1,
+ repoID: 0,
+ expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner2.ID),
+ },
+ {
+ name: "User runner visible to repository owned by user",
+ runner: runner2,
+ ownerID: 0,
+ repoID: repository1.ID,
+ expectedError: "",
+ },
+ {
+ name: "User runner invisible to repository owned by different user",
+ runner: runner2,
+ ownerID: 0,
+ repoID: repository32.ID,
+ expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner2.ID),
+ },
+ {
+ name: "Global runner",
+ runner: runner3,
+ ownerID: 0,
+ repoID: 0,
+ expectedError: "",
+ },
+ {
+ name: "Global runner is visible to any user",
+ runner: runner3,
+ ownerID: 2,
+ repoID: 0,
+ expectedError: "",
+ },
+ {
+ name: "Global runner is visible to any repository",
+ runner: runner3,
+ ownerID: 0,
+ repoID: repository32.ID,
+ expectedError: "",
+ },
+ {
+ name: "Repository runner",
+ runner: runner4,
+ ownerID: 0,
+ repoID: repository32.ID,
+ expectedError: "",
+ },
+ {
+ name: "Repository runner is visible to admins",
+ runner: runner4,
+ ownerID: 0,
+ repoID: 0,
+ expectedError: "",
+ },
+ {
+ name: "Repository runner is invisible to repository owner",
+ runner: runner4,
+ ownerID: repository32.OwnerID,
+ repoID: 0,
+ expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner4.ID),
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.name, func(t *testing.T) {
+ _, err := GetAvailableRunnerByID(t.Context(), testCase.runner.ID, testCase.ownerID, testCase.repoID)
+ if testCase.expectedError == "" {
+ require.NoError(t, err)
+ } else {
+ assert.ErrorContains(t, err, testCase.expectedError)
+ }
+ })
+ }
+}
diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json
index f87a3a6fdb..d358fcc424 100644
--- a/options/locale_next/locale_en-US.json
+++ b/options/locale_next/locale_en-US.json
@@ -421,13 +421,26 @@
"actions.runners.status.idle": "Idle",
"actions.runners.status.active": "Active",
"actions.runners.status.offline": "Offline",
- "actions.runners.id": "ID",
+ "actions.runners.uuid": "UUID",
"actions.runners.name": "Name",
"actions.runners.owner_type": "Type",
"actions.runners.description": "Description",
+ "actions.runners.token": "Token",
+ "actions.runners.ephemeral": "Ephemeral",
"actions.runners.labels": "Labels",
"actions.runners.last_online": "Last online time",
- "actions.runners.runner_title": "Runner",
+ "actions.runners.list_runners.details_column": "Details",
+ "actions.runners.list_runners.edit_column": "Edit",
+ "actions.runners.list_runners.delete_column": "Delete",
+ "actions.runners.list_runners.details_button": "Details",
+ "actions.runners.list_runners.details_button_aria": "Show details of %s",
+ "actions.runners.list_runners.delete_button": "Delete",
+ "actions.runners.list_runners.delete_button_aria": "Delete %s",
+ "actions.runners.list_runners.edit_button": "Edit",
+ "actions.runners.list_runners.edit_button_aria": "Edit %s",
+ "actions.runners.runner_title": "Runner %s",
+ "actions.runners.ephemeral.yes": "yes",
+ "actions.runners.ephemeral.no": "no",
"actions.runners.task_list": "Recent tasks on this runner",
"actions.runners.task_list.no_tasks": "There are no tasks yet.",
"actions.runners.task_list.run": "Run",
@@ -435,17 +448,47 @@
"actions.runners.task_list.repository": "Repository",
"actions.runners.task_list.commit": "Commit",
"actions.runners.task_list.done_at": "Done at",
- "actions.runners.edit_runner": "Edit runner",
- "actions.runners.update_runner.button": "Save changes",
+ "actions.runners.edit_runner_button": "Edit runner",
+ "actions.runners.create_runner.page_title": "Create new runner",
+ "actions.runners.create_runner.title": "Create new runner",
+ "actions.runners.create_runner.properties_fieldset": "Properties",
+ "actions.runners.create_runner.name_label": "Name*",
+ "actions.runners.create_runner.description_label": "Description",
+ "actions.runners.create_runner.create_button": "Create",
+ "actions.runners.create_runner.cancel_button": "Cancel",
+ "actions.runners.edit_runner.page_title": "Edit runner %s",
+ "actions.runners.edit_runner.title": "Edit runner %s",
+ "actions.runners.edit_runner.properties_fieldset": "Properties",
+ "actions.runners.edit_runner.properties_options": "Options",
+ "actions.runners.edit_runner.name_label": "Name*",
+ "actions.runners.edit_runner.description_label": "Description",
+ "actions.runners.edit_runner.regenerate_token_label": "Regenerate token",
+ "actions.runners.edit_runner.regenerate_token_help": "The existing token will be invalidated immediately. You will receive a new token on the next page.",
+ "actions.runners.edit_runner.save_button": "Save",
+ "actions.runners.edit_runner.cancel_button": "Cancel",
+ "actions.runners.runner_setup.title": "Set up runner %s",
+ "actions.runners.show_registration_token": "Show registration token",
"actions.runners.update_runner.success": "Runner edited successfully",
"actions.runners.update_runner.failed": "Failed to edit runner",
- "actions.runners.delete_runner.button": "Delete this runner",
- "actions.runners.delete_runner.success": "Runner deleted successfully",
- "actions.runners.delete_runner.failed": "Failed to delete runner",
"actions.runners.delete_runner.header": "Confirm to delete this runner",
"actions.runners.delete_runner.notice": "If a task is running on this runner, it will be terminated and marked as failed. It may break building workflow.",
+ "actions.runners.delete_runner.success": "Runner deleted successfully",
+ "actions.runners.delete_runner.failed": "Failed to delete runner",
+ "actions.runners.runner_details.page_title": "Runner %s",
+ "actions.runners.runner_details.labels_note": "The runner's labels are defined in the configuration file of Forgejo Runner or passed as command line option. They are updated every time Forgejo Runner establishes a connection to Forgejo.",
+ "actions.runners.runner_setup.page_title": "Set up runner %s",
+ "actions.runners.runner_setup.list_of_runners_link": "List of runners",
+ "actions.runners.runner_setup.last_chance_copying_token": "Copy the token now as you will not be able to see it again!",
+ "actions.runners.runner_setup.button_copy_uuid_aria": "Copy runner UUID",
+ "actions.runners.runner_setup.button_copy_token_aria": "Copy runner token",
+ "actions.runners.runner_setup.heading_using_configuration": "Using the runner configuration file",
+ "actions.runners.runner_setup.configuration_snippet_aria": "Snippet to insert into the runner configuration",
+ "actions.runners.runner_setup.program_options_snippet_aria": "How to invoke forgejo-runner",
+ "actions.runners.runner_setup.instruction_replace_connection_name": "Replace the connection name (forgejo in the example) with a value of your liking.",
+ "actions.runners.runner_setup.heading_using_options": "Using program options",
+ "actions.runners.runner_setup.instruction_advanced_configurations": "For configuring Forgejo Runner running in containers or advanced configurations, see the documentation.",
"actions.runners.none": "No runners available",
- "actions.runners.version": "Version",
+ "actions.runners.reset_registration_token.token": "Registration Token (Deprecated)",
"actions.runners.reset_registration_token.button": "Reset registration token",
"actions.runners.reset_registration_token.success": "Runner registration token reset successfully",
"actions.runs.run_attempt_label": "Run attempt #%[1]s (%[2]s)",
@@ -512,5 +555,6 @@
"editor.toggle_case": "Toggle case sensitivity",
"editor.toggle_regex": "Toggle using regular expressions",
"editor.toggle_whole_word": "Toggle matching whole words",
+ "form.RunnerName": "Name",
"meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it."
}
diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go
index 32c8667825..ab3ec32c4a 100644
--- a/routers/web/repo/setting/runners.go
+++ b/routers/web/repo/setting/runners.go
@@ -5,7 +5,7 @@ package setting
import (
"errors"
- "net/http"
+ "fmt"
"net/url"
actions_model "forgejo.org/models/actions"
@@ -18,88 +18,109 @@ import (
)
const (
- // TODO: Separate secrets from runners when layout is ready
- tplRepoRunners base.TplName = "repo/settings/actions"
- tplOrgRunners base.TplName = "org/settings/actions"
- tplAdminRunners base.TplName = "admin/actions"
- tplUserRunners base.TplName = "user/settings/actions"
- tplRepoRunnerEdit base.TplName = "repo/settings/runner_edit"
- tplOrgRunnerEdit base.TplName = "org/settings/runners_edit"
- tplAdminRunnerEdit base.TplName = "admin/runners/edit"
- tplUserRunnerEdit base.TplName = "user/settings/runner_edit"
+ tplAdminRunnerCreate base.TplName = "admin/runners/create"
+ tplAdminRunnerDetails base.TplName = "admin/runners/details"
+ tplAdminRunnerEdit base.TplName = "admin/runners/edit"
+ tplAdminRunnerSetup base.TplName = "admin/runners/setup"
+ tplAdminRunners base.TplName = "admin/actions"
+ tplOrgRunnerCreate base.TplName = "org/settings/runners_create"
+ tplOrgRunnerDetails base.TplName = "org/settings/runners_details"
+ tplOrgRunnerEdit base.TplName = "org/settings/runners_edit"
+ tplOrgRunnerSetup base.TplName = "org/settings/runners_setup"
+ tplOrgRunners base.TplName = "org/settings/actions"
+ tplRepoRunnerCreate base.TplName = "repo/settings/runner_create"
+ tplRepoRunnerDetails base.TplName = "repo/settings/runner_details"
+ tplRepoRunnerEdit base.TplName = "repo/settings/runner_edit"
+ tplRepoRunnerSetup base.TplName = "repo/settings/runner_setup"
+ tplRepoRunners base.TplName = "repo/settings/actions"
+ tplUserRunnerCreate base.TplName = "user/settings/runner_create"
+ tplUserRunnerDetails base.TplName = "user/settings/runner_details"
+ tplUserRunnerEdit base.TplName = "user/settings/runner_edit"
+ tplUserRunnerSetup base.TplName = "user/settings/runner_setup"
+ tplUserRunners base.TplName = "user/settings/actions"
)
type runnersCtx struct {
- OwnerID int64
- RepoID int64
- IsRepo bool
- IsOrg bool
- IsAdmin bool
- IsUser bool
- RunnersTemplate base.TplName
- RunnerEditTemplate base.TplName
- RedirectLink string
+ OwnerID int64
+ RepoID int64
+ IsRepo bool
+ IsOrg bool
+ IsAdmin bool
+ IsUser bool
+ RunnerCreateTemplate base.TplName
+ RunnerDetailsTemplate base.TplName
+ RunnerEditTemplate base.TplName
+ RunnerSetupTemplate base.TplName
+ RunnersTemplate base.TplName
+ RedirectLink string
}
func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
if ctx.Data["PageIsRepoSettings"] == true {
return &runnersCtx{
- RepoID: ctx.Repo.Repository.ID,
- OwnerID: 0,
- IsRepo: true,
- RunnersTemplate: tplRepoRunners,
- RunnerEditTemplate: tplRepoRunnerEdit,
- RedirectLink: ctx.Repo.RepoLink + "/settings/actions/runners/",
+ RepoID: ctx.Repo.Repository.ID,
+ OwnerID: 0,
+ IsRepo: true,
+ RunnerCreateTemplate: tplRepoRunnerCreate,
+ RunnerDetailsTemplate: tplRepoRunnerDetails,
+ RunnerEditTemplate: tplRepoRunnerEdit,
+ RunnerSetupTemplate: tplRepoRunnerSetup,
+ RunnersTemplate: tplRepoRunners,
+ RedirectLink: ctx.Repo.RepoLink + "/settings/actions/runners/",
}, nil
}
if ctx.Data["PageIsOrgSettings"] == true {
err := shared_user.LoadHeaderCount(ctx)
if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return nil, nil
+ return nil, fmt.Errorf("could not load project and package counts: %w", err)
}
return &runnersCtx{
- RepoID: 0,
- OwnerID: ctx.Org.Organization.ID,
- IsOrg: true,
- RunnersTemplate: tplOrgRunners,
- RunnerEditTemplate: tplOrgRunnerEdit,
- RedirectLink: ctx.Org.OrgLink + "/settings/actions/runners/",
+ RepoID: 0,
+ OwnerID: ctx.Org.Organization.ID,
+ IsOrg: true,
+ RunnerCreateTemplate: tplOrgRunnerCreate,
+ RunnerDetailsTemplate: tplOrgRunnerDetails,
+ RunnerEditTemplate: tplOrgRunnerEdit,
+ RunnerSetupTemplate: tplOrgRunnerSetup,
+ RunnersTemplate: tplOrgRunners,
+ RedirectLink: ctx.Org.OrgLink + "/settings/actions/runners/",
}, nil
}
if ctx.Data["PageIsAdmin"] == true {
return &runnersCtx{
- RepoID: 0,
- OwnerID: 0,
- IsAdmin: true,
- RunnersTemplate: tplAdminRunners,
- RunnerEditTemplate: tplAdminRunnerEdit,
- RedirectLink: setting.AppSubURL + "/admin/actions/runners/",
+ RepoID: 0,
+ OwnerID: 0,
+ IsAdmin: true,
+ RunnerCreateTemplate: tplAdminRunnerCreate,
+ RunnerDetailsTemplate: tplAdminRunnerDetails,
+ RunnerEditTemplate: tplAdminRunnerEdit,
+ RunnerSetupTemplate: tplAdminRunnerSetup,
+ RunnersTemplate: tplAdminRunners,
+ RedirectLink: setting.AppSubURL + "/admin/actions/runners/",
}, nil
}
if ctx.Data["PageIsUserSettings"] == true {
return &runnersCtx{
- OwnerID: ctx.Doer.ID,
- RepoID: 0,
- IsUser: true,
- RunnersTemplate: tplUserRunners,
- RunnerEditTemplate: tplUserRunnerEdit,
- RedirectLink: setting.AppSubURL + "/user/settings/actions/runners/",
+ OwnerID: ctx.Doer.ID,
+ RepoID: 0,
+ IsUser: true,
+ RunnerCreateTemplate: tplUserRunnerCreate,
+ RunnerDetailsTemplate: tplUserRunnerDetails,
+ RunnerEditTemplate: tplUserRunnerEdit,
+ RunnerSetupTemplate: tplUserRunnerSetup,
+ RunnersTemplate: tplUserRunners,
+ RedirectLink: setting.AppSubURL + "/user/settings/actions/runners/",
}, nil
}
return nil, errors.New("unable to set Runners context")
}
-// Runners render settings/actions/runners page for repo level
+// Runners renders the list of all available runners.
func Runners(ctx *context.Context) {
- ctx.Data["PageIsSharedSettingsRunners"] = true
- ctx.Data["Title"] = ctx.Tr("actions.actions")
- ctx.Data["PageType"] = "runners"
-
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
@@ -126,60 +147,114 @@ func Runners(ctx *context.Context) {
opts.OwnerID = rCtx.OwnerID
opts.WithAvailable = true
}
- actions_shared.RunnersList(ctx, opts)
- ctx.HTML(http.StatusOK, rCtx.RunnersTemplate)
+ ctx.Data["RunnersListLink"] = rCtx.RedirectLink
+
+ actions_shared.RunnersList(ctx, rCtx.RunnersTemplate, opts)
}
-// RunnersEdit renders runner edit page for repository level
-func RunnersEdit(ctx *context.Context) {
- ctx.Data["PageIsSharedSettingsRunners"] = true
- ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner")
+// RunnersDetails renders a read-only view of the most important properties of a runner. It is accessible to every user
+// that can use that particular runner.
+func RunnersDetails(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
return
}
+ runnerID := ctx.ParamsInt64(":runnerid")
page := ctx.FormInt("page")
if page <= 1 {
page = 1
}
- actions_shared.RunnerDetails(ctx, page,
- ctx.ParamsInt64(":runnerid"), rCtx.OwnerID, rCtx.RepoID,
- )
- ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate)
+ ctx.Data["RunnersListLink"] = rCtx.RedirectLink
+
+ actions_shared.RunnerDetails(ctx, runnerID, rCtx.OwnerID, rCtx.RepoID, rCtx.RunnerDetailsTemplate, page)
}
+// RunnersCreate renders the form for creating a new runner.
+func RunnersCreate(ctx *context.Context) {
+ rCtx, err := getRunnersCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getRunnersCtx", err)
+ return
+ }
+
+ ctx.Data["RunnersListLink"] = rCtx.RedirectLink
+
+ actions_shared.RunnerCreate(ctx, rCtx.RunnerCreateTemplate)
+}
+
+// RunnersCreatePost handles the form submitted by RunnersCreate.
+func RunnersCreatePost(ctx *context.Context) {
+ rCtx, err := getRunnersCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getRunnersCtx", err)
+ return
+ }
+
+ ctx.Data["RunnersListLink"] = rCtx.RedirectLink
+
+ actions_shared.RunnerCreatePost(ctx, rCtx.OwnerID, rCtx.RepoID, rCtx.RunnerCreateTemplate, rCtx.RunnerSetupTemplate)
+}
+
+// RunnersEdit renders the form for changing an existing runner.
+func RunnersEdit(ctx *context.Context) {
+ rCtx, err := getRunnersCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getRunnersCtx", err)
+ return
+ }
+
+ ctx.Data["RunnersListLink"] = rCtx.RedirectLink
+
+ actions_shared.RunnerEdit(ctx, ctx.ParamsInt64(":runnerid"), rCtx.OwnerID, rCtx.RepoID, rCtx.RunnerEditTemplate)
+}
+
+// RunnersEditPost handles the form submitted by RunnersEdit.
func RunnersEditPost(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
return
}
- actions_shared.RunnerDetailsEditPost(ctx, ctx.ParamsInt64(":runnerid"),
- rCtx.OwnerID, rCtx.RepoID,
- rCtx.RedirectLink+url.PathEscape(ctx.Params(":runnerid")))
+
+ ctx.Data["RunnersListLink"] = rCtx.RedirectLink
+
+ runnerID := ctx.ParamsInt64(":runnerid")
+ redirectURL := rCtx.RedirectLink + url.PathEscape(ctx.Params(":runnerid"))
+ actions_shared.RunnerEditPost(ctx, runnerID, rCtx.OwnerID, rCtx.RepoID, rCtx.RunnerEditTemplate,
+ rCtx.RunnerSetupTemplate, redirectURL)
}
+// ResetRunnerRegistrationToken handles the request to reset the runner registration token.
func ResetRunnerRegistrationToken(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
return
}
+
+ ctx.Data["RunnersListLink"] = rCtx.RedirectLink
+
actions_shared.RunnerResetRegistrationToken(ctx, rCtx.OwnerID, rCtx.RepoID, rCtx.RedirectLink)
}
-// RunnerDeletePost response for deleting runner
+// RunnerDeletePost handles the request to delete a runner.
func RunnerDeletePost(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
return
}
- actions_shared.RunnerDeletePost(ctx, ctx.ParamsInt64(":runnerid"), rCtx.OwnerID, rCtx.RepoID, rCtx.RedirectLink, rCtx.RedirectLink+url.PathEscape(ctx.Params(":runnerid")))
+
+ ctx.Data["RunnersListLink"] = rCtx.RedirectLink
+
+ runnerID := ctx.ParamsInt64(":runnerid")
+ successRedirectURL := rCtx.RedirectLink
+ failureRedirectURL := rCtx.RedirectLink
+ actions_shared.RunnerDeletePost(ctx, runnerID, rCtx.OwnerID, rCtx.RepoID, successRedirectURL, failureRedirectURL)
}
func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index 9d0ad3b0cd..ab52faf9f5 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -5,19 +5,24 @@ package actions
import (
"errors"
+ "net/http"
actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
+ "forgejo.org/modules/base"
"forgejo.org/modules/log"
"forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
"forgejo.org/modules/util"
"forgejo.org/modules/web"
"forgejo.org/services/context"
"forgejo.org/services/forms"
+
+ gouuid "github.com/google/uuid"
)
-// RunnersList prepares data for runners list
-func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) {
+// RunnersList renders the list of runners.
+func RunnersList(ctx *context.Context, template base.TplName, opts actions_model.FindRunnerOptions) {
runners, count, err := db.FindAndCount[actions_model.ActionRunner](ctx, opts)
if err != nil {
ctx.ServerError("CountRunners", err)
@@ -52,6 +57,9 @@ func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) {
return
}
+ ctx.Data["PageIsSharedSettingsRunners"] = true
+ ctx.Data["Title"] = ctx.Tr("actions.actions")
+ ctx.Data["PageType"] = "runners"
ctx.Data["Keyword"] = opts.Filter
ctx.Data["Runners"] = runners
ctx.Data["Total"] = count
@@ -63,26 +71,24 @@ func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) {
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
ctx.Data["Page"] = pager
+ ctx.HTML(http.StatusOK, template)
}
-// RunnerDetails prepares data for runners edit page
-func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int64) {
- runner, err := actions_model.GetRunnerByID(ctx, runnerID)
- if err != nil {
- ctx.ServerError("GetRunnerByID", err)
+// RunnerDetails displays detail information about each runner. The page is purely informational and visible to everyone
+// who is allowed to use a runner.
+func RunnerDetails(ctx *context.Context, runnerID, ownerID, repoID int64, template base.TplName, page int) {
+ runner, err := actions_model.GetAvailableRunnerByID(ctx, runnerID, ownerID, repoID)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.NotFound("GetAvailableRunnerByID", err)
+ return
+ } else if err != nil {
+ ctx.ServerError("GetAvailableRunnerByID", err)
return
}
if err := runner.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
- if !runner.Editable(ownerID, repoID) {
- err = errors.New("no permission to edit this runner")
- ctx.NotFound("RunnerDetails", err)
- return
- }
-
- ctx.Data["Runner"] = runner
opts := actions_model.FindTaskOptions{
ListOptions: db.ListOptions{
@@ -90,6 +96,8 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
PageSize: 30,
},
RunnerID: runner.ID,
+ OwnerID: ownerID,
+ RepoID: repoID,
}
tasks, count, err := db.FindAndCount[actions_model.ActionTask](ctx, opts)
@@ -103,42 +111,147 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
return
}
+ ctx.Data["PageIsSharedSettingsRunners"] = true
+ ctx.Data["RunnerOwnerID"] = ownerID
+ ctx.Data["RunnerRepoID"] = repoID
+ ctx.Data["Title"] = ctx.Tr("actions.runners.runner_details.page_title", runner.Name)
+ ctx.Data["Runner"] = runner
ctx.Data["Tasks"] = tasks
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
ctx.Data["Page"] = pager
+ ctx.HTML(http.StatusOK, template)
}
-// RunnerDetailsEditPost response for edit runner details
-func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64, redirectTo string) {
- runner, err := actions_model.GetRunnerByID(ctx, runnerID)
+// RunnerCreate displays a form for creating a new runner.
+func RunnerCreate(ctx *context.Context, template base.TplName) {
+ ctx.Data["PageIsSharedSettingsRunners"] = true
+ ctx.Data["Title"] = ctx.Tr("actions.runners.create_runner.page_title")
+ ctx.HTML(http.StatusOK, template)
+}
+
+// RunnerCreatePost handles the form submitted by RunnerCreate.
+func RunnerCreatePost(ctx *context.Context, ownerID, repoID int64, template, successTemplate base.TplName) {
+ form := web.GetForm(ctx).(*forms.CreateRunnerForm)
+
+ runner := actions_model.ActionRunner{
+ UUID: gouuid.New().String(),
+ Name: form.RunnerName,
+ OwnerID: ownerID,
+ RepoID: repoID,
+ Description: form.RunnerDescription,
+ Ephemeral: false,
+ }
+ runner.GenerateToken()
+
+ ctx.Data["PageIsSharedSettingsRunners"] = true
+ ctx.Data["Title"] = ctx.Tr("actions.runners.runner_setup.page_title", runner.Name)
+ ctx.Data["AppURL"] = setting.AppURL
+ ctx.Data["Runner"] = runner
+ ctx.Data["RunnerOwnerID"] = ownerID
+ ctx.Data["RunnerRepoID"] = repoID
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, template)
+ return
+ }
+
+ err := actions_model.CreateRunner(ctx, &runner)
if err != nil {
- log.Warn("RunnerDetailsEditPost.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL)
- ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err)
+ ctx.ServerError("CreateRunner", err)
+ return
+ }
+
+ ctx.HTML(http.StatusOK, successTemplate)
+}
+
+// RunnerEdit displays a form to modify the given runner.
+func RunnerEdit(ctx *context.Context, runnerID, ownerID, repoID int64, template base.TplName) {
+ runner, err := actions_model.GetAvailableRunnerByID(ctx, runnerID, ownerID, repoID)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.NotFound("GetAvailableRunnerByID", err)
+ return
+ } else if err != nil {
+ ctx.ServerError("GetAvailableRunnerByID", err)
+ return
+ }
+ if err := runner.LoadAttributes(ctx); err != nil {
+ ctx.ServerError("LoadAttributes", err)
return
}
if !runner.Editable(ownerID, repoID) {
- ctx.NotFound("RunnerDetailsEditPost.Editable", util.NewPermissionDeniedErrorf("no permission to edit this runner"))
+ err = errors.New("no permission to edit this runner")
+ ctx.NotFound("RunnerDetails", err)
return
}
- form := web.GetForm(ctx).(*forms.EditRunnerForm)
- runner.Description = form.Description
+ ctx.Data["PageIsSharedSettingsRunners"] = true
+ ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner.page_title", runner.Name)
+ ctx.Data["Runner"] = runner
+ ctx.Data["RunnerOwnerID"] = ownerID
+ ctx.Data["RunnerRepoID"] = repoID
+ ctx.HTML(http.StatusOK, template)
+}
- err = actions_model.UpdateRunner(ctx, runner, "description")
+// RunnerEditPost handles the form submitted by RunnerEdit.
+func RunnerEditPost(ctx *context.Context, runnerID, ownerID, repoID int64, template, successTemplate base.TplName, redirectTo string) {
+ runner, err := actions_model.GetAvailableRunnerByID(ctx, runnerID, ownerID, repoID)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.NotFound("GetAvailableRunnerByID", err)
+ return
+ } else if err != nil {
+ ctx.ServerError("GetAvailableRunnerByID", err)
+ return
+ }
+ if !runner.Editable(ownerID, repoID) {
+ ctx.NotFound("RunnerEditPost.Editable", util.NewPermissionDeniedErrorf("no permission to edit this runner"))
+ return
+ }
+
+ ctx.Data["PageIsSharedSettingsRunners"] = true
+ ctx.Data["Title"] = ctx.Tr("actions.runners.runner_setup.page_title", runner.Name)
+ ctx.Data["AppURL"] = setting.AppURL
+ ctx.Data["Runner"] = runner
+ ctx.Data["RunnerOwnerID"] = ownerID
+ ctx.Data["RunnerRepoID"] = repoID
+
+ form := web.GetForm(ctx).(*forms.EditRunnerForm)
+ runner.Name = form.RunnerName
+ runner.Description = form.RunnerDescription
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, template)
+ return
+ }
+
+ if !form.RegenerateToken {
+ err = actions_model.UpdateRunner(ctx, runner, "name", "description")
+ if err != nil {
+ log.Warn("RunnerEditPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
+ ctx.Flash.Warning(ctx.Tr("actions.runners.update_runner.failed"))
+ ctx.Redirect(redirectTo)
+ return
+ }
+
+ log.Debug("RunnerEditPost success: %s", ctx.Req.URL)
+
+ ctx.Flash.Success(ctx.Tr("actions.runners.update_runner.success"))
+ ctx.Redirect(redirectTo)
+ return
+ }
+
+ runner.GenerateToken()
+ err = actions_model.UpdateRunner(ctx, runner, "name", "description", "token_hash", "token_salt")
if err != nil {
- log.Warn("RunnerDetailsEditPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
+ log.Warn("RunnerEditPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
ctx.Flash.Warning(ctx.Tr("actions.runners.update_runner.failed"))
ctx.Redirect(redirectTo)
return
}
- log.Debug("RunnerDetailsEditPost success: %s", ctx.Req.URL)
-
- ctx.Flash.Success(ctx.Tr("actions.runners.update_runner.success"))
- ctx.Redirect(redirectTo)
+ ctx.HTML(http.StatusOK, successTemplate)
}
-// RunnerResetRegistrationToken reset registration token
+// RunnerResetRegistrationToken resets the runner registration token.
func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, redirectTo string) {
optOwnerID := optional.None[int64]()
if ownerID != 0 {
@@ -159,10 +272,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r
ctx.Redirect(redirectTo)
}
-// RunnerDeletePost response for deleting a runner
-func RunnerDeletePost(ctx *context.Context, runnerID, ownerID, repoID int64,
- successRedirectTo, failedRedirectTo string,
-) {
+// RunnerDeletePost handles the request for deleting a particular runner.
+func RunnerDeletePost(ctx *context.Context, runnerID, ownerID, repoID int64, successRedirectTo, failedRedirectTo string) {
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
ctx.ServerError("GetRunnerByID", err)
@@ -176,6 +287,7 @@ func RunnerDeletePost(ctx *context.Context, runnerID, ownerID, repoID int64,
if err := actions_model.DeleteRunner(ctx, runner); err != nil {
log.Warn("DeleteRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
+
ctx.Flash.Warning(ctx.Tr("actions.runners.delete_runner.failed"))
ctx.JSONRedirect(failedRedirectTo)
diff --git a/routers/web/shared/actions/runners_test.go b/routers/web/shared/actions/runners_test.go
index ad75d34ee6..9426bbdf08 100644
--- a/routers/web/shared/actions/runners_test.go
+++ b/routers/web/shared/actions/runners_test.go
@@ -25,14 +25,15 @@ func TestRunnerDetails(t *testing.T) {
t.Run("permission denied", func(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/admin/actions/runners")
- RunnerDetails(ctx, 1, runner.ID, user.ID, 0)
- assert.Equal(t, http.StatusNotFound, resp.Code)
+ RunnerDetails(ctx, runner.ID, user.ID, 0, "admin/runners/details", 1)
+ assert.Equal(t, http.StatusOK, resp.Code)
+ assert.Empty(t, ctx.GetData()["Tasks"])
})
t.Run("first page", func(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/admin/actions/runners")
page := 1
- RunnerDetails(ctx, page, runner.ID, 0, 0)
+ RunnerDetails(ctx, runner.ID, 0, 0, "admin/runners/details", page)
require.Equal(t, http.StatusOK, resp.Code)
assert.Len(t, ctx.GetData()["Tasks"], 30)
})
@@ -40,7 +41,7 @@ func TestRunnerDetails(t *testing.T) {
t.Run("second and last page", func(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/admin/actions/runners")
page := 2
- RunnerDetails(ctx, page, runner.ID, 0, 0)
+ RunnerDetails(ctx, runner.ID, 0, 0, "admin/runners/details", page)
require.Equal(t, http.StatusOK, resp.Code)
assert.Len(t, ctx.GetData()["Tasks"], 10)
})
diff --git a/routers/web/web.go b/routers/web/web.go
index 6cddc1d0b0..ee3a515dff 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -461,7 +461,12 @@ func registerRoutes(m *web.Route) {
addSettingsRunnersRoutes := func() {
m.Group("/runners", func() {
m.Get("", repo_setting.Runners)
- m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
+ m.Combo("/new").
+ Get(repo_setting.RunnersCreate).
+ Post(web.Bind(forms.CreateRunnerForm{}), repo_setting.RunnersCreatePost)
+ m.Get("/{runnerid}", repo_setting.RunnersDetails)
+ m.Combo("/{runnerid}/edit").
+ Get(repo_setting.RunnersEdit).
Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost)
m.Get("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
diff --git a/services/forms/runner.go b/services/forms/runner.go
index fcf6c5a694..319fffcee1 100644
--- a/services/forms/runner.go
+++ b/services/forms/runner.go
@@ -12,12 +12,26 @@ import (
"code.forgejo.org/go-chi/binding"
)
-// EditRunnerForm form for admin to create runner
-type EditRunnerForm struct {
- Description string
+// CreateRunnerForm needs to be filled in by the user to create a new runner.
+type CreateRunnerForm struct {
+ RunnerName string `binding:"Required;MaxSize(255)"`
+ RunnerDescription string
}
-// Validate validates form fields
+// Validate validates the submitted form.
+func (f *CreateRunnerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+ ctx := context.GetValidateContext(req)
+ return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
+
+// EditRunnerForm can be filled in by the user to change an existing runner.
+type EditRunnerForm struct {
+ RunnerName string `binding:"Required;MaxSize(255)"`
+ RunnerDescription string
+ RegenerateToken bool
+}
+
+// Validate validates the submitted form.
func (f *EditRunnerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
diff --git a/templates/admin/runners/create.tmpl b/templates/admin/runners/create.tmpl
new file mode 100644
index 0000000000..5d3bc739ac
--- /dev/null
+++ b/templates/admin/runners/create.tmpl
@@ -0,0 +1,5 @@
+{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
+
+ {{template "shared/actions/runner_create" .}}
+
+{{template "admin/layout_footer" .}}
diff --git a/templates/admin/runners/details.tmpl b/templates/admin/runners/details.tmpl
new file mode 100644
index 0000000000..47eb552952
--- /dev/null
+++ b/templates/admin/runners/details.tmpl
@@ -0,0 +1,5 @@
+{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
+
+ {{template "shared/actions/runner_details" .}}
+
+{{template "admin/layout_footer" .}}
diff --git a/templates/admin/runners/edit.tmpl b/templates/admin/runners/edit.tmpl
index 1165c84b79..6a4b696696 100644
--- a/templates/admin/runners/edit.tmpl
+++ b/templates/admin/runners/edit.tmpl
@@ -1,5 +1,5 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
-
- {{template "shared/actions/runner_edit" .}}
-
+
+ {{template "shared/actions/runner_edit" .}}
+
{{template "admin/layout_footer" .}}
diff --git a/templates/admin/runners/setup.tmpl b/templates/admin/runners/setup.tmpl
new file mode 100644
index 0000000000..6fa9f69a96
--- /dev/null
+++ b/templates/admin/runners/setup.tmpl
@@ -0,0 +1,5 @@
+{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
+
+ {{template "shared/actions/runner_setup" .}}
+
+{{template "admin/layout_footer" .}}
diff --git a/templates/org/settings/runners_create.tmpl b/templates/org/settings/runners_create.tmpl
new file mode 100644
index 0000000000..422d978811
--- /dev/null
+++ b/templates/org/settings/runners_create.tmpl
@@ -0,0 +1,5 @@
+{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings runners")}}
+
+ {{template "shared/actions/runner_create" .}}
+
+{{template "org/settings/layout_footer" .}}
diff --git a/templates/org/settings/runners_details.tmpl b/templates/org/settings/runners_details.tmpl
new file mode 100644
index 0000000000..367a4cd019
--- /dev/null
+++ b/templates/org/settings/runners_details.tmpl
@@ -0,0 +1,5 @@
+{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings runners")}}
+
+ {{template "shared/actions/runner_details" .}}
+
+{{template "org/settings/layout_footer" .}}
diff --git a/templates/org/settings/runners_setup.tmpl b/templates/org/settings/runners_setup.tmpl
new file mode 100644
index 0000000000..da377f3776
--- /dev/null
+++ b/templates/org/settings/runners_setup.tmpl
@@ -0,0 +1,5 @@
+{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings runners")}}
+
+ {{template "shared/actions/runner_setup" .}}
+
+{{template "org/settings/layout_footer" .}}
diff --git a/templates/repo/settings/runner_create.tmpl b/templates/repo/settings/runner_create.tmpl
new file mode 100644
index 0000000000..cf29555970
--- /dev/null
+++ b/templates/repo/settings/runner_create.tmpl
@@ -0,0 +1,5 @@
+{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings runners")}}
+
+ {{template "shared/actions/runner_create" .}}
+
+{{template "repo/settings/layout_footer" .}}
diff --git a/templates/repo/settings/runner_details.tmpl b/templates/repo/settings/runner_details.tmpl
new file mode 100644
index 0000000000..b79080c990
--- /dev/null
+++ b/templates/repo/settings/runner_details.tmpl
@@ -0,0 +1,5 @@
+{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings runners")}}
+
+ {{template "shared/actions/runner_details" .}}
+
+{{template "repo/settings/layout_footer" .}}
diff --git a/templates/repo/settings/runner_setup.tmpl b/templates/repo/settings/runner_setup.tmpl
new file mode 100644
index 0000000000..65e6f74e3f
--- /dev/null
+++ b/templates/repo/settings/runner_setup.tmpl
@@ -0,0 +1,5 @@
+{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings runners")}}
+
+ {{template "shared/actions/runner_setup" .}}
+
+{{template "repo/settings/layout_footer" .}}
diff --git a/templates/shared/actions/runner_create.tmpl b/templates/shared/actions/runner_create.tmpl
new file mode 100644
index 0000000000..9ee7bedde8
--- /dev/null
+++ b/templates/shared/actions/runner_create.tmpl
@@ -0,0 +1,22 @@
+
diff --git a/templates/shared/actions/runner_details.tmpl b/templates/shared/actions/runner_details.tmpl
new file mode 100644
index 0000000000..ee073c6098
--- /dev/null
+++ b/templates/shared/actions/runner_details.tmpl
@@ -0,0 +1,132 @@
+
+
+
+
+
+
- {{ctx.Locale.Tr "actions.runners.uuid"}}
+ -
+ {{.Runner.UUID}}
+
+
+
+
- {{ctx.Locale.Tr "actions.runners.owner_type"}}
+ -
+ {{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}
+
+
+
+
- {{ctx.Locale.Tr "actions.runners.labels"}}
+
-
+ {{if gt (len .Runner.AgentLabels) 0}}
+ {{range .Runner.AgentLabels}}
+
{{.}}
+ {{end}}
+ {{else}}
+ —
+ {{end}}
+
+
+
+
- {{ctx.Locale.Tr "actions.runners.last_online"}}
+
-
+
+ {{if .Runner.LastOnline}}
+ {{DateUtils.TimeSince .Runner.LastOnline}}
+ {{else}}
+ {{ctx.Locale.Tr "never"}}
+ {{end}}
+
+
+
+
+
- {{ctx.Locale.Tr "actions.runners.status"}}
+
-
+ {{if .Runner.IsActive}}
+
+ {{else if .Runner.IsIdle}}
+
+ {{else}}
+
+ {{end}}
+
+ {{.Runner.StatusLocaleName ctx.Locale}}
+
+
+
+
+
- {{ctx.Locale.Tr "actions.runners.ephemeral"}}
+ -
+ {{if .Runner.Ephemeral}}
+ {{ctx.Locale.Tr "actions.runners.ephemeral.yes"}}
+ {{else}}
+ {{ctx.Locale.Tr "actions.runners.ephemeral.no"}}
+ {{end}}
+
+
+
+
- {{ctx.Locale.Tr "actions.runners.description"}}
+ -
+ {{if .Runner.Description}}
+ {{.Runner.Description}}
+ {{else}}
+ —
+ {{end}}
+
+
+ {{ctx.Locale.Tr "actions.runners.runner_details.labels_note"}}
+
+
+
+
+
+
+
+
+ | {{ctx.Locale.Tr "actions.runners.task_list.run"}} |
+ {{ctx.Locale.Tr "actions.runners.task_list.status"}} |
+ {{ctx.Locale.Tr "actions.runners.task_list.repository"}} |
+ {{ctx.Locale.Tr "actions.runners.task_list.commit"}} |
+ {{ctx.Locale.Tr "actions.runners.task_list.done_at"}} |
+
+
+
+ {{range .Tasks}}
+
+ | {{.ID}} |
+ {{.Status.LocaleString ctx.Locale}} |
+ {{.GetRepoName}} |
+
+ {{ShortSha .CommitSHA}}
+ |
+ {{if .IsStopped}}
+ {{DateUtils.TimeSince .Stopped}}
+ {{else}}-{{end}} |
+
+ {{end}}
+ {{if not .Tasks}}
+
+ | {{ctx.Locale.Tr "actions.runners.task_list.no_tasks"}} |
+
+ {{end}}
+
+
+ {{template "base/paginate" .}}
+
+
diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl
index 66e419c520..749a85a6a3 100644
--- a/templates/shared/actions/runner_edit.tmpl
+++ b/templates/shared/actions/runner_edit.tmpl
@@ -1,95 +1,34 @@
-
-