mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: add form-based runner management (#11516)
Forgejo Runner is deprecating the runner registration token. It is too powerful, requires tooling, and is unnecessary. As a consequence, users need new mechanisms for managing runners in Forgejo. https://codeberg.org/forgejo/forgejo/pulls/10677 added an HTTP API for runner registration. This PR adds the ability to manage runners using Forgejo's web interface. Runners can be added, modified, and deleted. It is also possible to regenerate a runner's token. When a runner is added or a runner's token is regenerated, setup instructions are displayed. They explain how to alter Forgejo Runner's configuration file or how to launch `forgejo-runner daemon` (yet to be implemented). The existing details page has been overhauled and is now accessible to all users that are allowed to use a particular runner. The details page displays additional information that had to be removed from the list of runners due to space constraints. The task list is filtered. That means it only lists jobs of the respective repository, user, or organization. The runner registration token has been marked as deprecated. See https://code.forgejo.org/forgejo/forgejo-actions-feature-requests/issues/88 for context and design considerations. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes (can be removed for JavaScript 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 ### Tests for JavaScript changes (can be removed for Go changes) - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [x] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. *The decision if the pull request will be shown in the release notes is up to the mergers / release team.* The content of the `release-notes/<pull request number>.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11516 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch> Co-committed-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
This commit is contained in:
parent
c3908ba349
commit
2963572845
38 changed files with 2030 additions and 303 deletions
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (<code>forgejo</code> 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 <a href=\"https://TO-BE-REPLACED.COM\">documentation</a>.",
|
||||
"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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
5
templates/admin/runners/create.tmpl
Normal file
5
templates/admin/runners/create.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
|
||||
<div class="admin-setting-content">
|
||||
{{template "shared/actions/runner_create" .}}
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
5
templates/admin/runners/details.tmpl
Normal file
5
templates/admin/runners/details.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
|
||||
<div class="admin-setting-content">
|
||||
{{template "shared/actions/runner_details" .}}
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
|
||||
<div class="admin-setting-content">
|
||||
{{template "shared/actions/runner_edit" .}}
|
||||
</div>
|
||||
<div class="admin-setting-content">
|
||||
{{template "shared/actions/runner_edit" .}}
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
|
|
|
|||
5
templates/admin/runners/setup.tmpl
Normal file
5
templates/admin/runners/setup.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
|
||||
<div class="admin-setting-content">
|
||||
{{template "shared/actions/runner_setup" .}}
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
5
templates/org/settings/runners_create.tmpl
Normal file
5
templates/org/settings/runners_create.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings runners")}}
|
||||
<div class="org-setting-content">
|
||||
{{template "shared/actions/runner_create" .}}
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
||||
5
templates/org/settings/runners_details.tmpl
Normal file
5
templates/org/settings/runners_details.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings runners")}}
|
||||
<div class="org-setting-content">
|
||||
{{template "shared/actions/runner_details" .}}
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
||||
5
templates/org/settings/runners_setup.tmpl
Normal file
5
templates/org/settings/runners_setup.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings runners")}}
|
||||
<div class="org-setting-content">
|
||||
{{template "shared/actions/runner_setup" .}}
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
||||
5
templates/repo/settings/runner_create.tmpl
Normal file
5
templates/repo/settings/runner_create.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings runners")}}
|
||||
<div class="repo-setting-content">
|
||||
{{template "shared/actions/runner_create" .}}
|
||||
</div>
|
||||
{{template "repo/settings/layout_footer" .}}
|
||||
5
templates/repo/settings/runner_details.tmpl
Normal file
5
templates/repo/settings/runner_details.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings runners")}}
|
||||
<div class="repo-setting-content">
|
||||
{{template "shared/actions/runner_details" .}}
|
||||
</div>
|
||||
{{template "repo/settings/layout_footer" .}}
|
||||
5
templates/repo/settings/runner_setup.tmpl
Normal file
5
templates/repo/settings/runner_setup.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings runners")}}
|
||||
<div class="repo-setting-content">
|
||||
{{template "shared/actions/runner_setup" .}}
|
||||
</div>
|
||||
{{template "repo/settings/layout_footer" .}}
|
||||
22
templates/shared/actions/runner_create.tmpl
Normal file
22
templates/shared/actions/runner_create.tmpl
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<div class="runner-container">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "actions.runners.create_runner.title"}}
|
||||
</h4>
|
||||
<form class="ui form attached segment" action="{{.Link}}" method="post">
|
||||
<fieldset>
|
||||
<legend>{{ctx.Locale.Tr "actions.runners.create_runner.properties_fieldset"}}</legend>
|
||||
<div class="form-field">
|
||||
<label for="name">{{ctx.Locale.Tr "actions.runners.create_runner.name_label"}}</label>
|
||||
<input id="name" name="runner_name" type="text" value="{{.Runner.Name}}"{{if .Err_RunnerName}} class="error"{{end}}>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="description">{{ctx.Locale.Tr "actions.runners.create_runner.description_label"}}</label>
|
||||
<textarea id="description" name="runner_description"{{if .Err_RunnerDescription}} class="error"{{end}}>{{.Runner.Description}}</textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-buttons">
|
||||
<a class="ui secondary button" href="{{$.RunnersListLink}}">{{ctx.Locale.Tr "actions.runners.create_runner.cancel_button"}}</a>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "actions.runners.create_runner.create_button"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
132
templates/shared/actions/runner_details.tmpl
Normal file
132
templates/shared/actions/runner_details.tmpl
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<div class="runner-container">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "actions.runners.runner_title" .Runner.Name}}
|
||||
{{if .Runner.Editable $.RunnerOwnerID $.RunnerRepoID}}
|
||||
<div class="ui right">
|
||||
<a class="ui primary tiny button" tabindex="0" href="{{$.Link}}/edit">
|
||||
{{ctx.Locale.Tr "actions.runners.edit_runner_button"}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<dl aria-label="Properties of {{.Runner.Name}}">
|
||||
<div class="item">
|
||||
<dt>{{ctx.Locale.Tr "actions.runners.uuid"}}</dt>
|
||||
<dd>
|
||||
{{.Runner.UUID}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="item">
|
||||
<dt>{{ctx.Locale.Tr "actions.runners.owner_type"}}</dt>
|
||||
<dd>
|
||||
{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="item">
|
||||
<dt>{{ctx.Locale.Tr "actions.runners.labels"}}</dt>
|
||||
<dd class="tw-flex tw-items-start tw-flex-wrap tw-gap-2">
|
||||
{{if gt (len .Runner.AgentLabels) 0}}
|
||||
{{range .Runner.AgentLabels}}
|
||||
<div class="ui label">{{.}}</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
—
|
||||
{{end}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="item">
|
||||
<dt>{{ctx.Locale.Tr "actions.runners.last_online"}}</dt>
|
||||
<dd>
|
||||
<div>
|
||||
{{if .Runner.LastOnline}}
|
||||
{{DateUtils.TimeSince .Runner.LastOnline}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "never"}}
|
||||
{{end}}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="item">
|
||||
<dt>{{ctx.Locale.Tr "actions.runners.status"}}</dt>
|
||||
<dd class="tw-flex tw-items-center tw-gap-x-2">
|
||||
{{if .Runner.IsActive}}
|
||||
<div class="indicator-active">
|
||||
<div></div>
|
||||
</div>
|
||||
{{else if .Runner.IsIdle}}
|
||||
<div class="indicator-idle">
|
||||
<div></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="indicator-offline">
|
||||
<div></div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
{{.Runner.StatusLocaleName ctx.Locale}}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="item">
|
||||
<dt>{{ctx.Locale.Tr "actions.runners.ephemeral"}}</dt>
|
||||
<dd>
|
||||
{{if .Runner.Ephemeral}}
|
||||
{{ctx.Locale.Tr "actions.runners.ephemeral.yes"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "actions.runners.ephemeral.no"}}
|
||||
{{end}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="item">
|
||||
<dt>{{ctx.Locale.Tr "actions.runners.description"}}</dt>
|
||||
<dd>
|
||||
{{if .Runner.Description}}
|
||||
{{.Runner.Description}}
|
||||
{{else}}
|
||||
—
|
||||
{{end}}
|
||||
</dd>
|
||||
</div>
|
||||
<p class="tw-mt-8 tw-italic">{{ctx.Locale.Tr "actions.runners.runner_details.labels_note"}}</p>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "actions.runners.task_list"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.run"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.status"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.repository"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.commit"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.done_at"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Tasks}}
|
||||
<tr>
|
||||
<td><a href="{{.GetRunLink}}" target="_blank">{{.ID}}</a></td>
|
||||
<td><span class="ui label task-status-{{.Status.String}}">{{.Status.LocaleString ctx.Locale}}</span></td>
|
||||
<td><a href="{{.GetRepoLink}}" target="_blank">{{.GetRepoName}}</a></td>
|
||||
<td>
|
||||
<strong><a href="{{.GetCommitLink}}" target="_blank">{{ShortSha .CommitSHA}}</a></strong>
|
||||
</td>
|
||||
<td>{{if .IsStopped}}
|
||||
<span>{{DateUtils.TimeSince .Stopped}}</span>
|
||||
{{else}}-{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if not .Tasks}}
|
||||
<tr>
|
||||
<td colspan="5">{{ctx.Locale.Tr "actions.runners.task_list.no_tasks"}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{template "base/paginate" .}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,95 +1,34 @@
|
|||
<div class="runner-container">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "actions.runners.runner_title"}} {{.Runner.ID}} {{.Runner.Name}}
|
||||
{{ctx.Locale.Tr "actions.runners.edit_runner.title" .Runner.Name}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" method="post">
|
||||
{{template "base/disable_form_autofill"}}
|
||||
<div class="runner-basic-info">
|
||||
<div class="field tw-inline-block tw-mr-4">
|
||||
<label>{{ctx.Locale.Tr "actions.runners.status"}}</label>
|
||||
<span class="ui {{if .Runner.IsOnline}}green{{else}}basic{{end}} label">{{.Runner.StatusLocaleName ctx.Locale}}</span>
|
||||
<form class="ui form attached segment" action="{{.Link}}" method="post">
|
||||
<fieldset>
|
||||
<legend>{{ctx.Locale.Tr "actions.runners.edit_runner.properties_fieldset"}}</legend>
|
||||
<div class="form-field">
|
||||
<label for="name">{{ctx.Locale.Tr "actions.runners.edit_runner.name_label"}}</label>
|
||||
<input id="name" name="runner_name" type="text" value="{{.Runner.Name}}" {{if .Err_RunnerName}}class="error"{{end}}>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="description">{{ctx.Locale.Tr "actions.runners.edit_runner.description_label"}}</label>
|
||||
<textarea id="description" name="runner_description" {{if .Err_RunnerDescription}}class="error"{{end}}>{{.Runner.Description}}</textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ctx.Locale.Tr "actions.runners.edit_runner.properties_options"}}</legend>
|
||||
<div class="form-field tw-flex tw-gap-x-4">
|
||||
<div class="tw-flex tw-h-6 tw-items-center">
|
||||
<input type="checkbox" id="regenerate_token" name="regenerate_token">
|
||||
</div>
|
||||
<div class="field tw-inline-block tw-mr-4">
|
||||
<label>{{ctx.Locale.Tr "actions.runners.last_online"}}</label>
|
||||
<span>{{if .Runner.LastOnline}}{{DateUtils.TimeSince .Runner.LastOnline}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</span>
|
||||
</div>
|
||||
<div class="field tw-inline-block tw-mr-4">
|
||||
<label>{{ctx.Locale.Tr "actions.runners.labels"}}</label>
|
||||
<span>
|
||||
{{range .Runner.AgentLabels}}
|
||||
<span class="ui label">{{.}}</span>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="field tw-inline-block tw-mr-4">
|
||||
<label>{{ctx.Locale.Tr "actions.runners.owner_type"}}</label>
|
||||
<span data-tooltip-content="{{.Runner.BelongsToOwnerName}}">{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}</span>
|
||||
<div>
|
||||
<label for="regenerate_token" class="tw-font-medium tw-m-0">{{ctx.Locale.Tr "actions.runners.edit_runner.regenerate_token_label"}}</label>
|
||||
<p class="help">{{ctx.Locale.Tr "actions.runners.edit_runner.regenerate_token_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="field">
|
||||
<label for="description">{{ctx.Locale.Tr "actions.runners.description"}}</label>
|
||||
<input id="description" name="description" value="{{.Runner.Description}}">
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="field">
|
||||
<button class="ui primary button" data-url="{{.Link}}">{{ctx.Locale.Tr "actions.runners.update_runner.button"}}</button>
|
||||
<button class="ui red button delete-button" data-url="{{.Link}}/delete" data-modal-id="runner-delete-modal">
|
||||
{{ctx.Locale.Tr "actions.runners.delete_runner.button"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "actions.runners.task_list"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.run"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.status"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.repository"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.commit"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.done_at"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Tasks}}
|
||||
<tr>
|
||||
<td><a href="{{.GetRunLink}}" target="_blank">{{.ID}}</a></td>
|
||||
<td><span class="ui label task-status-{{.Status.String}}">{{.Status.LocaleString ctx.Locale}}</span></td>
|
||||
<td><a href="{{.GetRepoLink}}" target="_blank">{{.GetRepoName}}</a></td>
|
||||
<td>
|
||||
<strong><a href="{{.GetCommitLink}}" target="_blank">{{ShortSha .CommitSHA}}</a></strong>
|
||||
</td>
|
||||
<td>{{if .IsStopped}}
|
||||
<span>{{DateUtils.TimeSince .Stopped}}</span>
|
||||
{{else}}-{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if not .Tasks}}
|
||||
<tr>
|
||||
<td colspan="5">{{ctx.Locale.Tr "actions.runners.task_list.no_tasks"}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{template "base/paginate" .}}
|
||||
</div>
|
||||
<div class="ui g-modal-confirm delete modal" id="runner-delete-modal">
|
||||
<div class="header">
|
||||
{{svg "octicon-trash"}}
|
||||
{{ctx.Locale.Tr "actions.runners.delete_runner.header"}}
|
||||
</fieldset>
|
||||
<div class="form-buttons">
|
||||
<a class="ui secondary button" href="{{$.RunnersListLink}}">{{ctx.Locale.Tr "actions.runners.edit_runner.cancel_button"}}</a>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "actions.runners.edit_runner.save_button"}}</button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "actions.runners.delete_runner.notice"}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
|
||||
<div class="ui right">
|
||||
<div class="ui top right pointing dropdown">
|
||||
<button class="ui primary tiny button">
|
||||
{{ctx.Locale.Tr "actions.runners.new"}}
|
||||
<button class="ui secondary tiny button">
|
||||
{{ctx.Locale.Tr "actions.runners.show_registration_token"}}
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</button>
|
||||
<div class="menu">
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="header">
|
||||
Registration Token
|
||||
{{ctx.Locale.Tr "actions.runners.reset_registration_token.token"}}
|
||||
</div>
|
||||
<div class="ui input">
|
||||
<input type="text" value="{{.RegistrationToken}}">
|
||||
|
|
@ -28,7 +28,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="ui primary tiny button" href="{{$.Link}}/new" tabindex="0">
|
||||
{{ctx.Locale.Tr "actions.runners.new"}}
|
||||
</a>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
|
|
@ -37,59 +39,108 @@
|
|||
</form>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
{{if .Runners}}
|
||||
<table class="ui very basic striped table unstackable runner-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortt-asc="online" data-sortt-desc="offline">
|
||||
{{ctx.Locale.Tr "actions.runners.status"}}
|
||||
{{SortArrow "online" "offline" .SortType false}}
|
||||
</th>
|
||||
<th data-sortt-asc="newest" data-sortt-desc="oldest">
|
||||
{{ctx.Locale.Tr "actions.runners.id"}}
|
||||
{{SortArrow "oldest" "newest" .SortType false}}
|
||||
</th>
|
||||
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
|
||||
<th scope="col" data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
|
||||
{{ctx.Locale.Tr "actions.runners.name"}}
|
||||
{{SortArrow "alphabetically" "reversealphabetically" .SortType false}}
|
||||
</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.version"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.owner_type"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.labels"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.last_online"}}</th>
|
||||
<th>{{ctx.Locale.Tr "edit"}}</th>
|
||||
<th scope="col">
|
||||
{{ctx.Locale.Tr "actions.runners.labels"}}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{ctx.Locale.Tr "actions.runners.owner_type"}}
|
||||
</th>
|
||||
<th scope="col" data-sortt-asc="online" data-sortt-desc="offline">
|
||||
{{ctx.Locale.Tr "actions.runners.status"}}
|
||||
{{SortArrow "online" "offline" .SortType false}}
|
||||
</th>
|
||||
<th scope="col">
|
||||
<span class="tw-sr-only">{{ctx.Locale.Tr "actions.runners.list_runners.details_column"}}</span>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<span class="tw-sr-only">{{ctx.Locale.Tr "actions.runners.list_runners.edit_column"}}</span>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<span class="tw-sr-only">{{ctx.Locale.Tr "actions.runners.list_runners.delete_column"}}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if .Runners}}
|
||||
{{range .Runners}}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="ui {{if .IsOnline}}green{{end}} label">{{.StatusLocaleName ctx.Locale}}</span>
|
||||
</td>
|
||||
<td>{{.ID}}</td>
|
||||
<td><p data-tooltip-content="{{.Description}}">{{.Name}}</p></td>
|
||||
<td>{{if .Version}}{{.Version}}{{else}}{{ctx.Locale.Tr "unknown"}}{{end}}</td>
|
||||
<td><span data-tooltip-content="{{.BelongsToOwnerName}}">{{.BelongsToOwnerType.LocaleString ctx.Locale}}</span></td>
|
||||
<td class="tw-flex tw-flex-wrap tw-gap-2 runner-tags">
|
||||
{{range .AgentLabels}}<span class="ui label">{{.}}</span>{{end}}
|
||||
</td>
|
||||
<td>{{if .LastOnline}}{{DateUtils.TimeSince .LastOnline}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</td>
|
||||
<td class="runner-ops">
|
||||
{{if .Editable $.RunnerOwnerID $.RunnerRepoID}}
|
||||
<a href="{{$.Link}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
|
||||
{{range .Runners}}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="tw-font-medium">{{.Name}}</div>
|
||||
<div class="tw-mt-1">{{.UUID}}</div>
|
||||
</td>
|
||||
<td class="tw-flex tw-items-start tw-flex-wrap tw-gap-2">
|
||||
{{if gt (len .AgentLabels) 0}}
|
||||
{{range .AgentLabels}}
|
||||
<div class="ui label special">{{.}}</div>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td class="center aligned" colspan="8">{{ctx.Locale.Tr "actions.runners.none"}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
—
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{.BelongsToOwnerType.LocaleString ctx.Locale}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="tw-flex tw-items-center tw-gap-x-2">
|
||||
{{if .IsActive}}
|
||||
<div class="indicator-active">
|
||||
<div></div>
|
||||
</div>
|
||||
{{else if .IsIdle}}
|
||||
<div class="indicator-idle">
|
||||
<div></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="indicator-offline">
|
||||
<div></div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
{{.StatusLocaleName ctx.Locale}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="tw-text-right">
|
||||
<a href="{{$.Link}}/{{.ID}}" class="runner-action-link" tabindex="0" aria-label="{{ctx.Locale.Tr "actions.runners.list_runners.details_button_aria" .Name}}">{{ctx.Locale.Tr "actions.runners.list_runners.details_button"}}</a>
|
||||
</td>
|
||||
<td class="tw-text-right">
|
||||
{{if .Editable $.RunnerOwnerID $.RunnerRepoID}}
|
||||
<a href="{{$.Link}}/{{.ID}}/edit" class="runner-action-link" tabindex="0" aria-label="{{ctx.Locale.Tr "actions.runners.list_runners.edit_button_aria" .Name}}">{{ctx.Locale.Tr "actions.runners.list_runners.edit_button"}}</a>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="tw-text-right">
|
||||
{{if .Editable $.RunnerOwnerID $.RunnerRepoID}}
|
||||
<button class="delete-button runner-delete-link" aria-label="{{ctx.Locale.Tr "actions.runners.list_runners.delete_button_aria" .Name}}" data-url="{{$.Link}}/{{.ID}}/delete" data-modal-id="runner-delete-modal">{{ctx.Locale.Tr "actions.runners.list_runners.delete_button"}}</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="tw-flex tw-p-4">
|
||||
{{ctx.Locale.Tr "actions.runners.none"}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "base/paginate" .}}
|
||||
|
||||
<div class="ui g-modal-confirm delete modal" id="runner-delete-modal">
|
||||
<div class="header">
|
||||
{{svg "octicon-trash"}}
|
||||
{{ctx.Locale.Tr "actions.runners.delete_runner.header"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "actions.runners.delete_runner.notice"}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
53
templates/shared/actions/runner_setup.tmpl
Normal file
53
templates/shared/actions/runner_setup.tmpl
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<div class="runner-container">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "actions.runners.runner_setup.title" .Runner.Name}}
|
||||
<div class="ui right">
|
||||
<a class="ui secondary tiny button" href="{{.RunnersListLink}}" tabindex="0">
|
||||
{{ctx.Locale.Tr "actions.runners.runner_setup.list_of_runners_link"}}
|
||||
</a>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>{{ctx.Locale.Tr "actions.runners.runner_setup.last_chance_copying_token"}}</p>
|
||||
<dl>
|
||||
<div class="item">
|
||||
<dt>{{ctx.Locale.Tr "actions.runners.uuid"}}</dt>
|
||||
<dd>
|
||||
{{.Runner.UUID}}
|
||||
<button class="ui basic label button"
|
||||
aria-label="{{ctx.Locale.Tr "actions.runners.runner_setup.button_copy_uuid_aria"}}"
|
||||
data-clipboard-text="{{.Runner.UUID}}">
|
||||
{{svg "octicon-copy" 14}}
|
||||
</button>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="item">
|
||||
<dt>{{ctx.Locale.Tr "actions.runners.token"}}</dt>
|
||||
<dd>
|
||||
{{.Runner.Token}}
|
||||
<button class="ui basic label button"
|
||||
aria-label="{{ctx.Locale.Tr "actions.runners.runner_setup.button_copy_token_aria"}}"
|
||||
data-clipboard-text="{{.Runner.Token}}">
|
||||
{{svg "octicon-copy" 14}}
|
||||
</button>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<h5>{{ctx.Locale.Tr "actions.runners.runner_setup.heading_using_configuration"}}</h5>
|
||||
<pre><code aria-label="{{ctx.Locale.Tr "actions.runners.runner_setup.configuration_snippet_aria"}}">server:
|
||||
connections:
|
||||
forgejo:
|
||||
url: {{.AppURL}}
|
||||
uuid: {{.Runner.UUID}}
|
||||
token: {{.Runner.Token}}
|
||||
</code></pre>
|
||||
<p>{{ctx.Locale.Tr "actions.runners.runner_setup.instruction_replace_connection_name"}}</p>
|
||||
<h5>{{ctx.Locale.Tr "actions.runners.runner_setup.heading_using_options"}}</h5>
|
||||
<pre><code aria-label="{{ctx.Locale.Tr "actions.runners.runner_setup.program_options_snippet_aria"}}">forgejo-runner daemon \
|
||||
--url {{.AppURL}} \
|
||||
--uuid {{.Runner.UUID}} \
|
||||
--token {{.Runner.Token}}
|
||||
</code></pre>
|
||||
<p>{{ctx.Locale.Tr "actions.runners.runner_setup.instruction_advanced_configurations"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
5
templates/user/settings/runner_create.tmpl
Normal file
5
templates/user/settings/runner_create.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings runners")}}
|
||||
<div class="user-setting-content">
|
||||
{{template "shared/actions/runner_create" .}}
|
||||
</div>
|
||||
{{template "user/settings/layout_footer" .}}
|
||||
5
templates/user/settings/runner_details.tmpl
Normal file
5
templates/user/settings/runner_details.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings runners")}}
|
||||
<div class="user-setting-content">
|
||||
{{template "shared/actions/runner_details" .}}
|
||||
</div>
|
||||
{{template "user/settings/layout_footer" .}}
|
||||
5
templates/user/settings/runner_setup.tmpl
Normal file
5
templates/user/settings/runner_setup.tmpl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings runners")}}
|
||||
<div class="user-setting-content">
|
||||
{{template "shared/actions/runner_setup" .}}
|
||||
</div>
|
||||
{{template "user/settings/layout_footer" .}}
|
||||
|
|
@ -114,6 +114,9 @@ func TestE2e(t *testing.T) {
|
|||
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
|
||||
defer test.MockVariableValue(&testE2eWebRoutes, routers.NormalRoutes())()
|
||||
}
|
||||
if testname == "runner-management.test.e2e" {
|
||||
defer unittest.OverrideFixtures("tests/e2e/fixtures/runner-management")()
|
||||
}
|
||||
|
||||
// Default 2 minute timeout
|
||||
onForgejoRun(t, func(*testing.T, *url.URL) {
|
||||
|
|
|
|||
69
tests/e2e/fixtures/runner-management/action_runner.yml
Normal file
69
tests/e2e/fixtures/runner-management/action_runner.yml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
- id: 719931
|
||||
uuid: "8f940b0b-32a2-479a-9d48-06ab8d8a0b90"
|
||||
name: "runner-1"
|
||||
version: "dev"
|
||||
owner_id: 3
|
||||
repo_id: 0
|
||||
description: "A superb runner"
|
||||
agent_labels: ["debian", "gpu"]
|
||||
deleted: 0
|
||||
- id: 719932
|
||||
uuid: "3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
|
||||
name: "runner-2"
|
||||
version: "11.3.1"
|
||||
owner_id: 2
|
||||
repo_id: 0
|
||||
description: "An exclusive runner"
|
||||
agent_labels: ["docker"]
|
||||
# token: 9730f9d2c6c731f07582788d1a1fe72a6b999a17
|
||||
token_hash: ceb24f8ebc06fa5d0bb5a8dcab6f67b9bbc1f0c2f13313aefa0b81fe961ecc1feaad182d2b7f15fe6df0da8c17b12a9b11ef
|
||||
token_salt: 53GUbQ9nih0vBemnIxV5sU
|
||||
deleted: 0
|
||||
- id: 719933
|
||||
uuid: "11c9a6da-0a92-46ea-a4f1-b6c98f8c781c"
|
||||
name: "runner-3"
|
||||
version: "11.3.1"
|
||||
owner_id: 17
|
||||
repo_id: 0
|
||||
description: "Another fine runner"
|
||||
agent_labels: ["fedora"]
|
||||
deleted: 0
|
||||
- id: 719934
|
||||
uuid: "1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
|
||||
name: "runner-4"
|
||||
version: "12.2.0"
|
||||
owner_id: 0
|
||||
repo_id: 0
|
||||
description: "A runner for everyone"
|
||||
agent_labels: ["docker"]
|
||||
# token: 1ad04b98cafcc7461ef562b7ff07d1ade169eefb
|
||||
token_hash: 2aafaefc1690601106c0ccca874140828eb9f98e7719e58f3d399d4c64d11b1a251bd7363ff6fd4d1dbce852558987066334
|
||||
token_salt: 9--ISaaA6tHZbXMY6dqlqy
|
||||
deleted: 0
|
||||
- id: 719935
|
||||
uuid: "69d29449-1de5-4d17-845d-e3ae11a04a1b"
|
||||
name: "runner-5"
|
||||
version: "12.0.0"
|
||||
owner_id: 1
|
||||
repo_id: 0
|
||||
description: ""
|
||||
agent_labels: ["debian"]
|
||||
deleted: 0
|
||||
- id: 719936
|
||||
uuid: "9da25fbb-89a5-4520-a35a-d55fc94e4b76"
|
||||
name: "runner-6"
|
||||
version: "12.1.0"
|
||||
owner_id: 0
|
||||
repo_id: 62
|
||||
description: ""
|
||||
agent_labels: ["debian"]
|
||||
deleted: 0
|
||||
- id: 719937
|
||||
uuid: "d935307e-1d2d-4b61-8885-bc8a1c52c269"
|
||||
name: "runner-7"
|
||||
version: "12.1.0"
|
||||
owner_id: 4
|
||||
repo_id: 0
|
||||
description: ""
|
||||
agent_labels: ["alpine"]
|
||||
deleted: 0
|
||||
15
tests/e2e/fixtures/runner-management/action_task.yml
Normal file
15
tests/e2e/fixtures/runner-management/action_task.yml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
- id: 88931
|
||||
attempt: 1
|
||||
runner_id: 719934
|
||||
status: 6 # StatusRunning
|
||||
repo_id: 32
|
||||
owner_id: 3 # org3
|
||||
commit_sha: ed38c5a46c32eb907856178977fef0062cf407c3
|
||||
|
||||
- id: 88932
|
||||
attempt: 1
|
||||
runner_id: 719934
|
||||
status: 5 # StatusWaiting
|
||||
repo_id: 1
|
||||
owner_id: 2 # user2
|
||||
commit_sha: 49f55ab99b8ea6a84cae04d265822c93afdc3d50
|
||||
706
tests/e2e/runner-management.test.e2e.ts
Normal file
706
tests/e2e/runner-management.test.e2e.ts
Normal file
|
|
@ -0,0 +1,706 @@
|
|||
// @watch start
|
||||
// modules/actions/**
|
||||
// routers/web/shared/actions/**
|
||||
// routers/web/web.go
|
||||
// services/forms/runner.go
|
||||
// templates/admin/runners/**
|
||||
// templates/org/settings/runner*
|
||||
// templates/repo/settings/runner*
|
||||
// templates/shared/actions/runner*
|
||||
// templates/user/settings/runner*
|
||||
// web_src/css/actions.css
|
||||
// @watch end
|
||||
|
||||
import {test} from './utils_e2e.ts';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
const tokenPattern = /^[0-9a-f]{40}$/;
|
||||
|
||||
test.describe('Runners of user2', () => {
|
||||
test.use({user: 'user2'});
|
||||
|
||||
test('usable runners are visible', async ({page}) => {
|
||||
await page.goto('/user/settings/actions/runners');
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
|
||||
|
||||
const runnerContainer = page.locator('.runner-container');
|
||||
const rows = runnerContainer.getByRole('row');
|
||||
|
||||
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
|
||||
// the ordering of tests.
|
||||
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
|
||||
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
|
||||
- cell "docker"
|
||||
- cell "Individual"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-2"
|
||||
- cell "Edit runner-2"
|
||||
- cell "Delete runner-2"
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
|
||||
- cell "docker"
|
||||
- cell "Global"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-4"
|
||||
- cell
|
||||
- cell
|
||||
`);
|
||||
|
||||
await page.getByRole('link', {name: 'Show details of runner-2', exact: true}).click();
|
||||
await expect(page).toHaveTitle(/^Runner runner-2 .*/);
|
||||
|
||||
await page.goto('/user/settings/actions/runners');
|
||||
|
||||
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
|
||||
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
|
||||
});
|
||||
|
||||
test('runner details with tasks of repositories owned by user', async ({page}) => {
|
||||
await page.goto('/user/settings/actions/runners');
|
||||
|
||||
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
|
||||
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Properties of runner-4')).toMatchAriaSnapshot(`
|
||||
- term: UUID
|
||||
- definition: ${uuidPattern}
|
||||
- term: Type
|
||||
- definition: Global
|
||||
- term: Labels
|
||||
- definition: docker
|
||||
- term: Last online time
|
||||
- definition: Never
|
||||
- term: Status
|
||||
- definition: Offline
|
||||
- term: Ephemeral
|
||||
- definition: "no"
|
||||
- term: Description
|
||||
- definition: A runner for everyone
|
||||
`);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Recent tasks on this runner '})).toBeVisible();
|
||||
|
||||
const rows = page.getByRole('row');
|
||||
|
||||
// Only tasks from repositories owned by user2 should appear.
|
||||
await expect(rows).toHaveCount(2);
|
||||
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
|
||||
await expect(rows.nth(1)).toHaveAccessibleName('88932 Waiting 49f55ab99b -');
|
||||
});
|
||||
|
||||
test('create new runner', async ({page}) => {
|
||||
await page.goto('/user/settings/actions/runners');
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
|
||||
|
||||
await page.getByRole('link', {name: 'Create new runner'}).click();
|
||||
|
||||
await expect(page).toHaveTitle(/^Create new runner .*/);
|
||||
|
||||
// Submit an invalid form to test validation.
|
||||
await page.getByRole('button', {name: 'Create'}).click();
|
||||
await expect(page.getByRole('paragraph')).toHaveText('Name cannot be empty.');
|
||||
|
||||
// Submit a valid form to create a runner.
|
||||
await page.getByRole('textbox', {name: 'Name*'}).fill('runner-991301');
|
||||
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-991301');
|
||||
|
||||
await page.getByRole('button', {name: 'Create'}).click();
|
||||
|
||||
// Verify set up instructions.
|
||||
await expect(page).toHaveTitle(/^Set up runner runner-991301 .*/);
|
||||
await expect(page.getByRole('heading', {name: 'Set up runner runner-991301'})).toBeVisible();
|
||||
|
||||
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
|
||||
const runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
|
||||
expect(runnerUUID).toMatch(uuidPattern);
|
||||
|
||||
await page.getByRole('button', {name: 'Copy runner token'}).click();
|
||||
const runnerToken = await page.evaluate(() => navigator.clipboard.readText());
|
||||
expect(runnerToken).toMatch(tokenPattern);
|
||||
|
||||
await expect(page.getByRole('term')).toHaveText(['UUID', 'Token']);
|
||||
await expect(page.getByRole('definition')).toContainText([runnerUUID, runnerToken]);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Using the runner configuration file'})).toBeVisible();
|
||||
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`uuid: ${runnerUUID}`);
|
||||
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`token: ${runnerToken}`);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Using program options'})).toBeVisible();
|
||||
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--uuid ${runnerUUID}`);
|
||||
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--token ${runnerToken}`);
|
||||
|
||||
// Go back to list of runners.
|
||||
await page.getByRole('link', {name: 'List of runners', exact: true}).click();
|
||||
|
||||
await expect(page.locator(`tbody tr:has-text("${runnerUUID}")`)).toMatchAriaSnapshot(`
|
||||
- cell "runner-991301 ${runnerUUID}"
|
||||
- cell ""
|
||||
- cell "Individual"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-991301"
|
||||
- cell "Edit runner-991301"
|
||||
- cell "Delete runner-991301"
|
||||
`);
|
||||
});
|
||||
|
||||
test('edit runner without changing its token', async ({page}) => {
|
||||
await page.goto('/user/settings/actions/runners');
|
||||
|
||||
// We have to create a new runner because changes to fixtures would affect the remainder of the tests in this file.
|
||||
await page.getByRole('link', {name: 'Create new runner'}).click();
|
||||
await page.getByRole('textbox', {name: 'Name*'}).fill('runner-46635');
|
||||
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-46635');
|
||||
await page.getByRole('button', {name: 'Create'}).click();
|
||||
|
||||
// Go back to list of runners.
|
||||
await page.getByRole('link', {name: 'Runners', exact: true}).click();
|
||||
|
||||
// Edit the runner that was just created.
|
||||
await page.getByRole('link', {name: 'Edit runner-46635'}).click();
|
||||
|
||||
await expect(page).toHaveTitle(/^Edit runner runner-46635 .*/);
|
||||
await expect(page.getByRole('heading', {name: 'Edit runner runner-46635'})).toBeVisible();
|
||||
|
||||
// Make the form invalid to test validation.
|
||||
await page.getByRole('textbox', {name: 'Name*'}).clear();
|
||||
await page.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(page.locator('#flash-message')).toHaveText('Name cannot be empty.');
|
||||
await expect(page.getByRole('textbox', {name: 'Name*'})).toBeEmpty();
|
||||
await expect(page.getByRole('textbox', {name: 'Description'})).toHaveValue('Description of runner-46635');
|
||||
|
||||
// Submit a valid form.
|
||||
await page.getByRole('textbox', {name: 'Name*'}).fill('runner-46636');
|
||||
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-46636');
|
||||
|
||||
await page.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
// Verify that the runner's properties were updated properly.
|
||||
await expect(page).toHaveTitle(/^Runner runner-46636 .*/);
|
||||
await expect(page.locator('#flash-message')).toHaveText('Runner edited successfully');
|
||||
await expect(page.getByRole('heading', {name: 'Runner runner-46636'})).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Properties of runner-46636')).toMatchAriaSnapshot(`
|
||||
- term: UUID
|
||||
- definition: ${uuidPattern}
|
||||
- term: Type
|
||||
- definition: Individual
|
||||
- term: Labels
|
||||
- definition
|
||||
- term: Last online time
|
||||
- definition: Never
|
||||
- term: Status
|
||||
- definition: Offline
|
||||
- term: Ephemeral
|
||||
- definition: "no"
|
||||
- term: Description
|
||||
- definition: Description of runner-46636
|
||||
`);
|
||||
});
|
||||
|
||||
test('regenerate runner token', async ({page}) => {
|
||||
await page.goto('/user/settings/actions/runners');
|
||||
|
||||
await page.getByRole('link', {name: 'Edit runner-2', exact: true}).click();
|
||||
|
||||
await expect(page).toHaveTitle(/^Edit runner runner-2 .*/);
|
||||
await expect(page.getByRole('heading', {name: 'Edit runner runner-2'})).toBeVisible();
|
||||
|
||||
await page.getByRole('checkbox', {name: 'Regenerate token'}).check();
|
||||
await page.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
// Verify set up instructions.
|
||||
await expect(page).toHaveTitle(/^Set up runner runner-2 .*/);
|
||||
await expect(page.getByRole('heading', {name: 'Set up runner runner-2'})).toBeVisible();
|
||||
|
||||
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
|
||||
const runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
|
||||
expect(runnerUUID).toEqual('3a20ad8d-d5d6-4b7b-ba55-841ac8264c17');
|
||||
|
||||
await page.getByRole('button', {name: 'Copy runner token'}).click();
|
||||
const runnerToken = await page.evaluate(() => navigator.clipboard.readText());
|
||||
expect(runnerToken).not.toEqual('9730f9d2c6c731f07582788d1a1fe72a6b999a17');
|
||||
expect(runnerToken).toMatch(tokenPattern);
|
||||
|
||||
await expect(page.getByRole('term')).toHaveText(['UUID', 'Token']);
|
||||
await expect(page.getByRole('definition')).toContainText([runnerUUID, runnerToken]);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Using the runner configuration file'})).toBeVisible();
|
||||
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`uuid: ${runnerUUID}`);
|
||||
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`token: ${runnerToken}`);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Using program options'})).toBeVisible();
|
||||
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--uuid ${runnerUUID}`);
|
||||
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--token ${runnerToken}`);
|
||||
});
|
||||
|
||||
test('delete runner', async ({page}) => {
|
||||
await page.goto('/user/settings/actions/runners');
|
||||
|
||||
// We have to create a new runner because changes to fixtures affect the remainder of the tests in this file.
|
||||
await page.getByRole('link', {name: 'Create new runner'}).click();
|
||||
await page.getByRole('textbox', {name: 'Name*'}).fill('runner-660332');
|
||||
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-660332');
|
||||
await page.getByRole('button', {name: 'Create'}).click();
|
||||
|
||||
// Go back to list of runners.
|
||||
await page.getByRole('link', {name: 'Runners', exact: true}).click();
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
|
||||
await expect(page.getByRole('document')).toContainText('runner-660332');
|
||||
|
||||
// Delete the runner that was just created.
|
||||
await page.getByRole('button', {name: 'Delete runner-660332'}).click();
|
||||
|
||||
// Confirm deletion
|
||||
await expect(page.getByRole('document')).toContainText('Confirm to delete this runner');
|
||||
|
||||
await page.getByRole('button', {name: 'Yes', exact: true}).click();
|
||||
|
||||
// Verify that the runner is gone.
|
||||
await expect(page.locator('#flash-message')).toHaveText('Runner deleted successfully');
|
||||
await expect(page.getByRole('document')).not.toContainText('runner-660332');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Global runners', () => {
|
||||
test.use({user: 'user1'});
|
||||
|
||||
test('all runners are visible', async ({page}) => {
|
||||
await page.goto('/admin/actions/runners');
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
|
||||
|
||||
const runnerContainer = page.locator('.runner-container');
|
||||
const rows = runnerContainer.getByRole('row');
|
||||
|
||||
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
|
||||
// the ordering of tests.
|
||||
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
|
||||
await expect(page.locator('tbody tr:has-text("8f940b0b-32a2-479a-9d48-06ab8d8a0b90")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-1 8f940b0b-32a2-479a-9d48-06ab8d8a0b90"
|
||||
- cell "debian gpu"
|
||||
- cell "Organization"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-1"
|
||||
- cell "Edit runner-1"
|
||||
- cell "Delete runner-1"
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
|
||||
- cell "docker"
|
||||
- cell "Individual"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-2"
|
||||
- cell "Edit runner-2"
|
||||
- cell "Delete runner-2"
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("11c9a6da-0a92-46ea-a4f1-b6c98f8c781c")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-3 11c9a6da-0a92-46ea-a4f1-b6c98f8c781c"
|
||||
- cell "fedora"
|
||||
- cell "Organization"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-3"
|
||||
- cell "Edit runner-3"
|
||||
- cell "Delete runner-3"
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
|
||||
- cell "docker"
|
||||
- cell "Global"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-4"
|
||||
- cell "Edit runner-4"
|
||||
- cell "Delete runner-4"
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("69d29449-1de5-4d17-845d-e3ae11a04a1b")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-5 69d29449-1de5-4d17-845d-e3ae11a04a1b"
|
||||
- cell "debian"
|
||||
- cell "Individual"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-5"
|
||||
- cell "Edit runner-5"
|
||||
- cell "Delete runner-5"
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("9da25fbb-89a5-4520-a35a-d55fc94e4b76")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-6 9da25fbb-89a5-4520-a35a-d55fc94e4b76"
|
||||
- cell "debian"
|
||||
- cell "Repository"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-6"
|
||||
- cell "Edit runner-6"
|
||||
- cell "Delete runner-6"
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("d935307e-1d2d-4b61-8885-bc8a1c52c269")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-7 d935307e-1d2d-4b61-8885-bc8a1c52c269"
|
||||
- cell "alpine"
|
||||
- cell "Individual"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-7"
|
||||
- cell "Edit runner-7"
|
||||
- cell "Delete runner-7"
|
||||
`);
|
||||
});
|
||||
|
||||
test('runner details with all tasks visible on details page', async ({page}) => {
|
||||
await page.goto('/admin/actions/runners');
|
||||
|
||||
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
|
||||
|
||||
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
|
||||
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Properties of runner-4')).toMatchAriaSnapshot(`
|
||||
- term: UUID
|
||||
- definition: ${uuidPattern}
|
||||
- term: Type
|
||||
- definition: Global
|
||||
- term: Labels
|
||||
- definition: docker
|
||||
- term: Last online time
|
||||
- definition: Never
|
||||
- term: Status
|
||||
- definition: Offline
|
||||
- term: Ephemeral
|
||||
- definition: "no"
|
||||
- term: Description
|
||||
- definition: A runner for everyone
|
||||
`);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Recent tasks on this runner '})).toBeVisible();
|
||||
|
||||
const rows = page.getByRole('row');
|
||||
|
||||
// Only tasks from repositories owned by user2 should appear.
|
||||
await expect(rows).toHaveCount(3);
|
||||
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
|
||||
await expect(rows.nth(1)).toHaveAccessibleName('88932 Waiting 49f55ab99b -');
|
||||
await expect(rows.nth(2)).toHaveAccessibleName('88931 Running ed38c5a46c -');
|
||||
});
|
||||
|
||||
test('create new runner', async ({page}) => {
|
||||
await page.goto('/admin/actions/runners');
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
|
||||
|
||||
await page.getByRole('link', {name: 'Create new runner'}).click();
|
||||
|
||||
await expect(page).toHaveTitle(/^Create new runner .*/);
|
||||
|
||||
// Submit an invalid form to test validation.
|
||||
await page.getByRole('button', {name: 'Create'}).click();
|
||||
await expect(page.getByRole('paragraph')).toHaveText('Name cannot be empty.');
|
||||
|
||||
// Submit a valid form to create a runner.
|
||||
await page.getByRole('textbox', {name: 'Name*'}).fill('runner-473465');
|
||||
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-473465');
|
||||
|
||||
await page.getByRole('button', {name: 'Create'}).click();
|
||||
|
||||
// Verify set up instructions.
|
||||
await expect(page).toHaveTitle(/^Set up runner runner-473465 .*/);
|
||||
await expect(page.getByRole('heading', {name: 'Set up runner runner-473465'})).toBeVisible();
|
||||
|
||||
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
|
||||
const runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
|
||||
expect(runnerUUID).toMatch(uuidPattern);
|
||||
|
||||
await page.getByRole('button', {name: 'Copy runner token'}).click();
|
||||
const runnerToken = await page.evaluate(() => navigator.clipboard.readText());
|
||||
expect(runnerToken).toMatch(tokenPattern);
|
||||
|
||||
await expect(page.getByRole('term')).toHaveText(['UUID', 'Token']);
|
||||
await expect(page.getByRole('definition')).toContainText([runnerUUID, runnerToken]);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Using the runner configuration file'})).toBeVisible();
|
||||
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`uuid: ${runnerUUID}`);
|
||||
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`token: ${runnerToken}`);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Using program options'})).toBeVisible();
|
||||
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--uuid ${runnerUUID}`);
|
||||
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--token ${runnerToken}`);
|
||||
|
||||
// Go back to list of runners.
|
||||
await page.getByRole('link', {name: 'List of runners', exact: true}).click();
|
||||
|
||||
await expect(page.locator(`tbody tr:has-text("${runnerUUID}")`)).toMatchAriaSnapshot(`
|
||||
- cell "runner-473465 ${runnerUUID}"
|
||||
- cell ""
|
||||
- cell "Global"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-473465"
|
||||
- cell "Edit runner-473465"
|
||||
- cell "Delete runner-473465"
|
||||
`);
|
||||
});
|
||||
|
||||
test('edit runner without changing its token', async ({page}) => {
|
||||
await page.goto('/admin/actions/runners');
|
||||
|
||||
// We have to create a new runner because changes to fixtures would affect the remainder of the tests in this file.
|
||||
await page.getByRole('link', {name: 'Create new runner'}).click();
|
||||
await page.getByRole('textbox', {name: 'Name*'}).fill('runner-956857');
|
||||
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-956857');
|
||||
await page.getByRole('button', {name: 'Create'}).click();
|
||||
|
||||
// Go back to list of runners.
|
||||
await page.getByRole('link', {name: 'Runners', exact: true}).click();
|
||||
|
||||
// Edit the runner that was just created.
|
||||
await page.getByRole('link', {name: 'Edit runner-956857'}).click();
|
||||
|
||||
await expect(page).toHaveTitle(/^Edit runner runner-956857 .*/);
|
||||
await expect(page.getByRole('heading', {name: 'Edit runner runner-956857'})).toBeVisible();
|
||||
|
||||
// Make the form invalid to test validation.
|
||||
await page.getByRole('textbox', {name: 'Name*'}).clear();
|
||||
await page.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(page.locator('#flash-message')).toHaveText('Name cannot be empty.');
|
||||
await expect(page.getByRole('textbox', {name: 'Name*'})).toBeEmpty();
|
||||
await expect(page.getByRole('textbox', {name: 'Description'})).toHaveValue('Description of runner-956857');
|
||||
|
||||
// Submit a valid form.
|
||||
await page.getByRole('textbox', {name: 'Name*'}).fill('runner-956858');
|
||||
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-956858');
|
||||
|
||||
await page.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
// Verify that the runner's properties were updated properly.
|
||||
await expect(page).toHaveTitle(/^Runner runner-956858 .*/);
|
||||
await expect(page.locator('#flash-message')).toHaveText('Runner edited successfully');
|
||||
await expect(page.getByRole('heading', {name: 'Runner runner-956858'})).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Properties of runner-956858')).toMatchAriaSnapshot(`
|
||||
- term: UUID
|
||||
- definition: ${uuidPattern}
|
||||
- term: Type
|
||||
- definition: Global
|
||||
- term: Labels
|
||||
- definition
|
||||
- term: Last online time
|
||||
- definition: Never
|
||||
- term: Status
|
||||
- definition: Offline
|
||||
- term: Ephemeral
|
||||
- definition: "no"
|
||||
- term: Description
|
||||
- definition: Description of runner-956858
|
||||
`);
|
||||
});
|
||||
|
||||
test('delete runner', async ({page}) => {
|
||||
await page.goto('/admin/actions/runners');
|
||||
|
||||
// We have to create a new runner because changes to fixtures affect the remainder of the tests in this file.
|
||||
await page.getByRole('link', {name: 'Create new runner'}).click();
|
||||
await page.getByRole('textbox', {name: 'Name*'}).fill('runner-650332');
|
||||
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-650332');
|
||||
await page.getByRole('button', {name: 'Create'}).click();
|
||||
|
||||
// Go back to list of runners.
|
||||
await page.getByRole('link', {name: 'Runners', exact: true}).click();
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
|
||||
await expect(page.getByRole('document')).toContainText('runner-650332');
|
||||
|
||||
// Delete the runner that was just created.
|
||||
await page.getByRole('button', {name: 'Delete runner-650332'}).click();
|
||||
|
||||
// Confirm deletion
|
||||
await expect(page.getByRole('document')).toContainText('Confirm to delete this runner');
|
||||
|
||||
await page.getByRole('button', {name: 'Yes', exact: true}).click();
|
||||
|
||||
// Verify that the runner is gone.
|
||||
await expect(page.locator('#flash-message')).toHaveText('Runner deleted successfully');
|
||||
await expect(page.getByRole('document')).not.toContainText('runner-650332');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Organization runners', () => {
|
||||
test.use({user: 'user2'});
|
||||
|
||||
test('usable runners are visible', async ({page}) => {
|
||||
await page.goto('/org/org3/settings/actions/runners');
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
|
||||
|
||||
const runnerContainer = page.locator('.runner-container');
|
||||
const rows = runnerContainer.getByRole('row');
|
||||
|
||||
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
|
||||
// the ordering of tests.
|
||||
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
|
||||
await expect(page.locator('tbody tr:has-text("8f940b0b-32a2-479a-9d48-06ab8d8a0b90")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-1 8f940b0b-32a2-479a-9d48-06ab8d8a0b90"
|
||||
- cell "debian gpu"
|
||||
- cell "Organization"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-1"
|
||||
- cell "Edit runner-1"
|
||||
- cell "Delete runner-1"
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
|
||||
- cell "docker"
|
||||
- cell "Global"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-4"
|
||||
- cell
|
||||
- cell
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toBeHidden();
|
||||
await expect(page.locator('tbody tr:has-text("11c9a6da-0a92-46ea-a4f1-b6c98f8c781c")')).toBeHidden();
|
||||
await expect(page.locator('tbody tr:has-text("69d29449-1de5-4d17-845d-e3ae11a04a1b")')).toBeHidden();
|
||||
await expect(page.locator('tbody tr:has-text("9da25fbb-89a5-4520-a35a-d55fc94e4b76")')).toBeHidden();
|
||||
await expect(page.locator('tbody tr:has-text("d935307e-1d2d-4b61-8885-bc8a1c52c269")')).toBeHidden();
|
||||
|
||||
// Verify that details of usable runners are accessible.
|
||||
await page.getByRole('link', {name: 'Show details of runner-1', exact: true}).click();
|
||||
await expect(page).toHaveTitle(/^Runner runner-1 .*/);
|
||||
|
||||
await page.goto('/org/org3/settings/actions/runners');
|
||||
|
||||
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
|
||||
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
|
||||
});
|
||||
|
||||
test('runner details with tasks of repositories owned by organization', async ({page}) => {
|
||||
await page.goto('/org/org3/settings/actions/runners');
|
||||
|
||||
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
|
||||
|
||||
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
|
||||
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Properties of runner-4')).toMatchAriaSnapshot(`
|
||||
- term: UUID
|
||||
- definition: ${uuidPattern}
|
||||
- term: Type
|
||||
- definition: Global
|
||||
- term: Labels
|
||||
- definition: docker
|
||||
- term: Last online time
|
||||
- definition: Never
|
||||
- term: Status
|
||||
- definition: Offline
|
||||
- term: Ephemeral
|
||||
- definition: "no"
|
||||
- term: Description
|
||||
- definition: A runner for everyone
|
||||
`);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Recent tasks on this runner '})).toBeVisible();
|
||||
|
||||
const rows = page.getByRole('row');
|
||||
|
||||
// Only tasks from repositories owned by org3 should appear.
|
||||
await expect(rows).toHaveCount(2);
|
||||
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
|
||||
await expect(rows.nth(1)).toHaveAccessibleName('88931 Running ed38c5a46c -');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Repository runners', () => {
|
||||
test.use({user: 'user2'});
|
||||
|
||||
test('usable runners are visible', async ({page}) => {
|
||||
await page.goto('/user2/test_workflows/settings/actions/runners');
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
|
||||
|
||||
const runnerContainer = page.locator('.runner-container');
|
||||
const rows = runnerContainer.getByRole('row');
|
||||
|
||||
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
|
||||
// the ordering of tests.
|
||||
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
|
||||
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
|
||||
- cell "docker"
|
||||
- cell "Individual"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-2"
|
||||
- cell
|
||||
- cell
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
|
||||
- cell "docker"
|
||||
- cell "Global"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-4"
|
||||
- cell
|
||||
- cell
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("9da25fbb-89a5-4520-a35a-d55fc94e4b76")')).toMatchAriaSnapshot(`
|
||||
- cell "runner-6 9da25fbb-89a5-4520-a35a-d55fc94e4b76"
|
||||
- cell "debian"
|
||||
- cell "Repository"
|
||||
- cell "Offline"
|
||||
- cell "Show details of runner-6"
|
||||
- cell "Edit runner-6"
|
||||
- cell "Delete runner-6"
|
||||
`);
|
||||
await expect(page.locator('tbody tr:has-text("11c9a6da-0a92-46ea-a4f1-b6c98f8c781c")')).toBeHidden();
|
||||
await expect(page.locator('tbody tr:has-text("69d29449-1de5-4d17-845d-e3ae11a04a1b")')).toBeHidden();
|
||||
await expect(page.locator('tbody tr:has-text("d935307e-1d2d-4b61-8885-bc8a1c52c269")')).toBeHidden();
|
||||
|
||||
// Verify that details of usable runners are accessible.
|
||||
await page.getByRole('link', {name: 'Show details of runner-2', exact: true}).click();
|
||||
await expect(page).toHaveTitle(/^Runner runner-2 .*/);
|
||||
|
||||
await page.goto('/user2/test_workflows/settings/actions/runners');
|
||||
|
||||
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
|
||||
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
|
||||
|
||||
await page.goto('/user2/test_workflows/settings/actions/runners');
|
||||
|
||||
await page.getByRole('link', {name: 'Show details of runner-6', exact: true}).click();
|
||||
await expect(page).toHaveTitle(/^Runner runner-6 .*/);
|
||||
});
|
||||
|
||||
test('runner details with tasks of repository only', async ({page}) => {
|
||||
await page.goto('/user2/test_workflows/settings/actions/runners');
|
||||
|
||||
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
|
||||
|
||||
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
|
||||
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Properties of runner-4')).toMatchAriaSnapshot(`
|
||||
- term: UUID
|
||||
- definition: ${uuidPattern}
|
||||
- term: Type
|
||||
- definition: Global
|
||||
- term: Labels
|
||||
- definition: docker
|
||||
- term: Last online time
|
||||
- definition: Never
|
||||
- term: Status
|
||||
- definition: Offline
|
||||
- term: Ephemeral
|
||||
- definition: "no"
|
||||
- term: Description
|
||||
- definition: A runner for everyone
|
||||
`);
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Recent tasks on this runner '})).toBeVisible();
|
||||
|
||||
const rows = page.getByRole('row');
|
||||
|
||||
// Only tasks from this repository should appear.
|
||||
await expect(rows).toHaveCount(2);
|
||||
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
|
||||
await expect(rows.nth(1)).toHaveAccessibleName('There are no tasks yet.');
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,9 @@
|
|||
owner_id: 2
|
||||
repo_id: 0
|
||||
deleted: 0
|
||||
# token: 9730f9d2c6c731f07582788d1a1fe72a6b999a17
|
||||
token_hash: ceb24f8ebc06fa5d0bb5a8dcab6f67b9bbc1f0c2f13313aefa0b81fe961ecc1feaad182d2b7f15fe6df0da8c17b12a9b11ef
|
||||
token_salt: 53GUbQ9nih0vBemnIxV5sU
|
||||
|
||||
-
|
||||
id: 1002
|
||||
|
|
@ -13,6 +16,9 @@
|
|||
owner_id: 3
|
||||
repo_id: 0
|
||||
deleted: 0
|
||||
# token: 724e44135713fd2b4d44f3c75955b020bff979ea
|
||||
token_hash: 3cc123529e77ae1a7e7b77d480a376dddd85f22dae23cf1dcbf552bf1e2afeacce7dee03ddbf4fe55f15c2c31de051069c01
|
||||
token_salt: afwDV_UMPp_Xm2T8-D6WIt
|
||||
|
||||
-
|
||||
id: 1003
|
||||
|
|
@ -21,6 +27,9 @@
|
|||
owner_id: 0
|
||||
repo_id: 1
|
||||
deleted: 0
|
||||
# token: 15ef826cbcba0e46c5aa873fc0639bf9a599bc2b
|
||||
token_hash: 5b4fb4aadf3b7529a6d165f3e785b51e7c2ab812fd427550c438c350662f35ce64b7a5170a60ac49ca322991416d0cf60fdc
|
||||
token_salt: 98TOI49tvsPkWIgl_CJ14y
|
||||
|
||||
-
|
||||
id: 1004
|
||||
|
|
@ -29,3 +38,6 @@
|
|||
owner_id: 0
|
||||
repo_id: 0
|
||||
deleted: 0
|
||||
# token: 98a9f9eaa63683f1328e9b80e547f70507f22864
|
||||
token_hash: 49c9a05828e5d416896420334e30a6c209be407039007ad82728ad3029e4a229373457c68a13a6a06e40998274bbdb4bdd83
|
||||
token_salt: gqTeYixe5YwmCkc4pwbVAH
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package integration
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
|
|
@ -15,6 +16,7 @@ import (
|
|||
app_context "forgejo.org/services/context"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -47,8 +49,11 @@ func TestRunnerModification(t *testing.T) {
|
|||
sess = adminSess
|
||||
}
|
||||
|
||||
req := NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d", id), map[string]string{
|
||||
"description": "New Description",
|
||||
originalRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
|
||||
req := NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d/edit", id), map[string]string{
|
||||
"runner_name": "New Name",
|
||||
"runner_description": "New Description",
|
||||
})
|
||||
if fail {
|
||||
sess.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
|
@ -57,6 +62,12 @@ func TestRunnerModification(t *testing.T) {
|
|||
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DRunner%2Bedited%2Bsuccessfully", flashCookie.Value)
|
||||
|
||||
// Verify that the runner's token isn't changed during a normal update when token regeneration is not
|
||||
// requested.
|
||||
updatedRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.Equal(t, originalRunner.TokenHash, updatedRunner.TokenHash, "token was changed unexpectedly")
|
||||
assert.Equal(t, originalRunner.TokenSalt, updatedRunner.TokenSalt, "token was changed unexpectedly")
|
||||
}
|
||||
|
||||
req = NewRequest(t, "POST", baseURL+fmt.Sprintf("/%d/delete", id))
|
||||
|
|
@ -136,58 +147,132 @@ func TestRunnerVisibility(t *testing.T) {
|
|||
runnerFive := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 719935})
|
||||
runnerSix := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 719936})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *user_model.User
|
||||
url string
|
||||
expectedRunners []*actions_model.ActionRunner
|
||||
unexpectedRunners []*actions_model.ActionRunner
|
||||
}{
|
||||
{
|
||||
name: "admin-sees-all",
|
||||
user: admin,
|
||||
url: "/admin/actions/runners",
|
||||
expectedRunners: []*actions_model.ActionRunner{runnerOne, runnerTwo, runnerThree, runnerFour, runnerFive, runnerSix},
|
||||
unexpectedRunners: []*actions_model.ActionRunner{},
|
||||
},
|
||||
{
|
||||
name: "user-sees-own-and-global",
|
||||
user: user2,
|
||||
url: "user/settings/actions/runners",
|
||||
expectedRunners: []*actions_model.ActionRunner{runnerTwo, runnerFour},
|
||||
unexpectedRunners: []*actions_model.ActionRunner{runnerOne, runnerThree, runnerFive, runnerSix},
|
||||
},
|
||||
{
|
||||
name: "org-sees-own-and-global",
|
||||
user: user2,
|
||||
url: "/org/org3/settings/actions/runners",
|
||||
expectedRunners: []*actions_model.ActionRunner{runnerOne, runnerFour},
|
||||
unexpectedRunners: []*actions_model.ActionRunner{runnerTwo, runnerThree, runnerFive, runnerSix},
|
||||
},
|
||||
{
|
||||
name: "user-repo-sees-own-and-users-and-global",
|
||||
user: user2,
|
||||
url: "/user2/test_workflows/settings/actions/runners",
|
||||
expectedRunners: []*actions_model.ActionRunner{runnerTwo, runnerFour, runnerSix},
|
||||
unexpectedRunners: []*actions_model.ActionRunner{runnerOne, runnerThree, runnerFive},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
session := loginUser(t, testCase.user.Name)
|
||||
|
||||
request := NewRequest(t, "GET", testCase.url)
|
||||
response := session.MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, response.Body)
|
||||
for _, expectedRunner := range testCase.expectedRunners {
|
||||
selector := fmt.Sprintf("td:contains('%s')", expectedRunner.Name)
|
||||
assert.Equal(t, 1, htmlDoc.Find(selector).Length(), "runner '%s' could not be found", expectedRunner.Name)
|
||||
}
|
||||
for _, unexpectedRunner := range testCase.unexpectedRunners {
|
||||
selector := fmt.Sprintf("td:contains('%s')", unexpectedRunner.Name)
|
||||
assert.Zero(t, htmlDoc.Find(selector).Length(), "runner '%s' is unexpectedly present", unexpectedRunner.Name)
|
||||
}
|
||||
containsText := func(selection *goquery.Selection, text string) bool {
|
||||
filtered := selection.FilterFunction(func(i int, s *goquery.Selection) bool {
|
||||
return strings.Contains(strings.TrimSpace(s.Text()), text)
|
||||
})
|
||||
return filtered.Length() == 1
|
||||
}
|
||||
|
||||
t.Run("runner list", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *user_model.User
|
||||
url string
|
||||
expectedRunners []*actions_model.ActionRunner
|
||||
unexpectedRunners []*actions_model.ActionRunner
|
||||
}{
|
||||
{
|
||||
name: "Admin sees all",
|
||||
user: admin,
|
||||
url: "/admin/actions/runners",
|
||||
expectedRunners: []*actions_model.ActionRunner{runnerOne, runnerTwo, runnerThree, runnerFour, runnerFive, runnerSix},
|
||||
unexpectedRunners: []*actions_model.ActionRunner{},
|
||||
},
|
||||
{
|
||||
name: "User sees own and global",
|
||||
user: user2,
|
||||
url: "/user/settings/actions/runners",
|
||||
expectedRunners: []*actions_model.ActionRunner{runnerTwo, runnerFour},
|
||||
unexpectedRunners: []*actions_model.ActionRunner{runnerOne, runnerThree, runnerFive, runnerSix},
|
||||
},
|
||||
{
|
||||
name: "Org sees own and global",
|
||||
user: user2,
|
||||
url: "/org/org3/settings/actions/runners",
|
||||
expectedRunners: []*actions_model.ActionRunner{runnerOne, runnerFour},
|
||||
unexpectedRunners: []*actions_model.ActionRunner{runnerTwo, runnerThree, runnerFive, runnerSix},
|
||||
},
|
||||
{
|
||||
name: "User repo sees own and user's and global",
|
||||
user: user2,
|
||||
url: "/user2/test_workflows/settings/actions/runners",
|
||||
expectedRunners: []*actions_model.ActionRunner{runnerTwo, runnerFour, runnerSix},
|
||||
unexpectedRunners: []*actions_model.ActionRunner{runnerOne, runnerThree, runnerFive},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
session := loginUser(t, testCase.user.Name)
|
||||
|
||||
request := NewRequest(t, "GET", testCase.url)
|
||||
response := session.MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, response.Body)
|
||||
for _, expectedRunner := range testCase.expectedRunners {
|
||||
selector := fmt.Sprintf("td:contains('%s')", expectedRunner.Name)
|
||||
assert.Equal(t, 1, htmlDoc.Find(selector).Length(), "runner '%s' could not be found", expectedRunner.Name)
|
||||
}
|
||||
for _, unexpectedRunner := range testCase.unexpectedRunners {
|
||||
selector := fmt.Sprintf("td:contains('%s')", unexpectedRunner.Name)
|
||||
assert.Zero(t, htmlDoc.Find(selector).Length(), "runner '%s' is unexpectedly present", unexpectedRunner.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("runner details", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *user_model.User
|
||||
runner *actions_model.ActionRunner
|
||||
accessibleURLs []string
|
||||
inaccessibleURLs []string
|
||||
}{
|
||||
{
|
||||
name: "Organization runner",
|
||||
user: user2,
|
||||
runner: runnerOne,
|
||||
// Actions are disabled on all repositories of org3. That's why runnerOne isn't accessible in any
|
||||
// repository.
|
||||
accessibleURLs: []string{"/org/org3/settings/actions/runners"},
|
||||
inaccessibleURLs: []string{"/user/settings/actions/runners", "/user2/test_workflows/settings/actions/runners"},
|
||||
},
|
||||
{
|
||||
name: "User runner",
|
||||
user: user2,
|
||||
runner: runnerTwo,
|
||||
accessibleURLs: []string{"/user/settings/actions/runners", "/user2/test_workflows/settings/actions/runners"},
|
||||
inaccessibleURLs: []string{"/org/org3/settings/actions/runners"},
|
||||
},
|
||||
{
|
||||
name: "Global runner",
|
||||
user: user2,
|
||||
runner: runnerFour,
|
||||
accessibleURLs: []string{
|
||||
"/user/settings/actions/runners",
|
||||
"/user2/test_workflows/settings/actions/runners",
|
||||
"/org/org3/settings/actions/runners",
|
||||
},
|
||||
inaccessibleURLs: []string{},
|
||||
},
|
||||
{
|
||||
name: "Repository runner",
|
||||
user: user2,
|
||||
runner: runnerSix,
|
||||
accessibleURLs: []string{"/user2/test_workflows/settings/actions/runners"},
|
||||
inaccessibleURLs: []string{"/user/settings/actions/runners", "/org/org3/settings/actions/runners"},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
session := loginUser(t, testCase.user.Name)
|
||||
|
||||
for _, accessibleURL := range testCase.accessibleURLs {
|
||||
request := NewRequest(t, "GET", fmt.Sprintf("%s/%d", accessibleURL, testCase.runner.ID))
|
||||
response := session.MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, response.Body)
|
||||
assert.True(t, containsText(htmlDoc.Find("dd"), testCase.runner.UUID))
|
||||
}
|
||||
for _, inaccessibleURL := range testCase.inaccessibleURLs {
|
||||
request := NewRequest(t, "GET", fmt.Sprintf("%s/%d", inaccessibleURL, testCase.runner.ID))
|
||||
response := session.MakeRequest(t, request, http.StatusNotFound)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, response.Body)
|
||||
assert.False(t, containsText(htmlDoc.Find("body"), testCase.runner.UUID))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,100 @@
|
|||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.runner-container table.runner-list thead th {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.runner-container table.runner-list th, .runner-container table.runner-list td {
|
||||
padding-block: 1rem !important;
|
||||
padding-inline: 0.75rem !important;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.indicator-offline, .indicator-idle, .indicator-active {
|
||||
flex: none;
|
||||
border-radius: calc(infinity * 1px);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.indicator-offline {
|
||||
color: var(--color-indicator-offline);
|
||||
background-color: var(--color-indicator-offline-20);
|
||||
}
|
||||
|
||||
.indicator-idle {
|
||||
color: var(--color-indicator-idle);
|
||||
background-color: var(--color-indicator-idle-20);
|
||||
}
|
||||
|
||||
.indicator-active {
|
||||
color: var(--color-indicator-active);
|
||||
background-color: var(--color-indicator-active-20);
|
||||
}
|
||||
|
||||
.runner-container .indicator-offline > div, .runner-container .indicator-idle > div, .runner-container .indicator-active > div {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
background-color: currentcolor;
|
||||
}
|
||||
|
||||
.runner-action-link {
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.runner-action-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.runner-delete-link {
|
||||
background: transparent;
|
||||
color: var(--color-red);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.runner-delete-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.runner-container form .form-field {
|
||||
margin-block: 1rem !important;
|
||||
}
|
||||
|
||||
.runner-container form .form-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.runner-container pre {
|
||||
color: var(--color-console-fg);
|
||||
background-color: var(--color-console-bg);
|
||||
border-color: var(--color-console-border);
|
||||
border-width: 1px;
|
||||
border-radius: 0.5rem;
|
||||
border-style: dotted;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.runner-container dl .item {
|
||||
display: flex;
|
||||
padding-block: 0.5rem
|
||||
}
|
||||
|
||||
.runner-container dl dt {
|
||||
flex: none;
|
||||
font-weight: var(--font-weight-medium);
|
||||
width: 12rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.runner-container dl dd {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.run-list-item-right {
|
||||
width: 130px;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -196,7 +196,6 @@
|
|||
--color-orange-badge: #ea580c;
|
||||
--color-orange-badge-bg: #ea580c22;
|
||||
--color-orange-badge-hover-bg: #ea580c44;
|
||||
|
||||
/* Colors for thin elements: octicons, text, borders */
|
||||
--thin-lightness: 0.68;
|
||||
--regular-chroma: 0.19;
|
||||
|
|
@ -281,6 +280,12 @@
|
|||
/* pattern colors for image diff */
|
||||
--checkerboard-color-1: #474747;
|
||||
--checkerboard-color-2: #313131;
|
||||
--color-indicator-offline: #a1a1aa;
|
||||
--color-indicator-offline-20: #a1a1aa1a;
|
||||
--color-indicator-idle: #16a34a;
|
||||
--color-indicator-idle-20: #16a34a1a;
|
||||
--color-indicator-active: #2185d0;
|
||||
--color-indicator-active-20: #2185d033;
|
||||
accent-color: var(--color-accent);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -279,6 +279,12 @@
|
|||
/* pattern colors for gradient */
|
||||
--checkerboard-color-1: #ffffff;
|
||||
--checkerboard-color-2: #e5e5e5;
|
||||
--color-indicator-offline: #52525b;
|
||||
--color-indicator-offline-20: #52525b33;
|
||||
--color-indicator-idle: #16a34a;
|
||||
--color-indicator-idle-20: #16a34a33;
|
||||
--color-indicator-active: #2185d0;
|
||||
--color-indicator-active-20: #2185d033;
|
||||
accent-color: var(--color-accent);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,6 +239,12 @@
|
|||
/* pattern colors for image diff */
|
||||
--checkerboard-color-1: #313131;
|
||||
--checkerboard-color-2: #212121;
|
||||
--color-indicator-offline: #a1a1aa;
|
||||
--color-indicator-offline-20: #a1a1aa1a;
|
||||
--color-indicator-idle: #16a34a;
|
||||
--color-indicator-idle-20: #16a34a1a;
|
||||
--color-indicator-active: #2185d0;
|
||||
--color-indicator-active-20: #2185d033;
|
||||
accent-color: var(--color-accent);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,6 +239,12 @@
|
|||
/* pattern colors for gradient */
|
||||
--checkerboard-color-1: #ffffff;
|
||||
--checkerboard-color-2: #e5e5e5;
|
||||
--color-indicator-offline: #52525b;
|
||||
--color-indicator-offline-20: #52525b1a;
|
||||
--color-indicator-idle: #16a34a;
|
||||
--color-indicator-idle-20: #16a34a1a;
|
||||
--color-indicator-active: #2185d0;
|
||||
--color-indicator-active-20: #2185d033;
|
||||
accent-color: var(--color-accent);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue