Add support for workflow_dispatch (#3334)
Closes #2797 I'm aware of https://github.com/go-gitea/gitea/pull/28163 exists, but since I had it laying around on my drive and collecting dust, I might as well open a PR for it if anyone wants the feature a bit sooner than waiting for upstream to release it or to be a forgejo "native" implementation. This PR Contains: - Support for the `workflow_dispatch` trigger - Inputs: boolean, string, number, choice Things still to be done: - [x] API Endpoint `/api/v1/<org>/<repo>/actions/workflows/<workflow id>/dispatches` - ~~Fixing some UI bugs I had no time figuring out, like why dropdown/choice inputs's menu's behave weirdly~~ Unrelated visual bug with dropdowns inside dropdowns - [x] Fix bug where opening the branch selection submits the form - [x] Limit on inputs to render/process Things not in this PR: - Inputs: environment (First need support for environments in forgejo) Things needed to test this: - A patch for https://code.forgejo.org/forgejo/runner to actually consider the inputs inside the workflow. ~~One possible patch can be seen here: https://code.forgejo.org/Mai-Lapyst/runner/src/branch/support-workflow-inputs~~ [PR](https://code.forgejo.org/forgejo/runner/pulls/199) ![image](/attachments/2db50c9e-898f-41cb-b698-43edeefd2573) ## Testing - Checkout PR - Setup new development runner with [this PR](https://code.forgejo.org/forgejo/runner/pulls/199) - Create a repo with a workflow (see below) - Go to the actions tab, select the workflow and see the notice as in the screenshot above - Use the button + dropdown to run the workflow - Try also running it via the api using the `` endpoint - ... - Profit! <details> <summary>Example workflow</summary> ```yaml on: workflow_dispatch: inputs: logLevel: description: 'Log Level' required: true default: 'warning' type: choice options: - info - warning - debug tags: description: 'Test scenario tags' required: false type: boolean boolean_default_true: description: 'Test scenario tags' required: true type: boolean default: true boolean_default_false: description: 'Test scenario tags' required: false type: boolean default: false number1_default: description: 'Number w. default' default: '100' type: number number2: description: 'Number w/o. default' type: number string1_default: description: 'String w. default' default: 'Hello world' type: string string2: description: 'String w/o. default' required: true type: string jobs: test: runs-on: docker steps: - uses: actions/checkout@v3 - run: whoami - run: cat /etc/issue - run: uname -a - run: date - run: echo ${{ inputs.logLevel }} - run: echo ${{ inputs.tags }} - env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - run: echo "abc" ``` </details> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3334 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Mai-Lapyst <mai-lapyst@noreply.codeberg.org> Co-committed-by: Mai-Lapyst <mai-lapyst@noreply.codeberg.org>
This commit is contained in:
parent
544cbc6f01
commit
51735c415b
39 changed files with 792 additions and 16 deletions
|
@ -2714,6 +2714,8 @@ LEVEL = Info
|
|||
;ABANDONED_JOB_TIMEOUT = 24h
|
||||
;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
|
||||
;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip]
|
||||
;; Limit on inputs for manual / workflow_dispatch triggers, default is 10
|
||||
;LIMIT_DISPATCH_INPUTS = 10
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -750,3 +750,41 @@
|
|||
type: 3
|
||||
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 108
|
||||
repo_id: 62
|
||||
type: 1
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 109
|
||||
repo_id: 62
|
||||
type: 2
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 110
|
||||
repo_id: 62
|
||||
type: 3
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 111
|
||||
repo_id: 62
|
||||
type: 4
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 112
|
||||
repo_id: 62
|
||||
type: 5
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 113
|
||||
repo_id: 62
|
||||
type: 10
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
|
|
@ -1782,3 +1782,33 @@
|
|||
size: 0
|
||||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
||||
|
||||
- id: 62
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: test_workflows
|
||||
name: test_workflows
|
||||
default_branch: main
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
num_milestones: 0
|
||||
num_closed_milestones: 0
|
||||
num_projects: 0
|
||||
num_closed_projects: 0
|
||||
is_private: false
|
||||
is_empty: false
|
||||
is_archived: false
|
||||
is_mirror: false
|
||||
status: 0
|
||||
is_fork: false
|
||||
fork_id: 0
|
||||
is_template: false
|
||||
template_id: 0
|
||||
size: 0
|
||||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
|
@ -66,7 +66,7 @@
|
|||
num_followers: 2
|
||||
num_following: 1
|
||||
num_stars: 2
|
||||
num_repos: 16
|
||||
num_repos: 17
|
||||
num_teams: 0
|
||||
num_members: 0
|
||||
visibility: 0
|
||||
|
|
|
@ -138,27 +138,27 @@ func getTestCases() []struct {
|
|||
{
|
||||
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
|
||||
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)},
|
||||
count: 34,
|
||||
count: 35,
|
||||
},
|
||||
{
|
||||
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
|
||||
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)},
|
||||
count: 39,
|
||||
count: 40,
|
||||
},
|
||||
{
|
||||
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
|
||||
opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true},
|
||||
count: 15,
|
||||
count: 16,
|
||||
},
|
||||
{
|
||||
name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName",
|
||||
opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true},
|
||||
count: 13,
|
||||
count: 14,
|
||||
},
|
||||
{
|
||||
name: "AllPublic/PublicRepositoriesOfOrganization",
|
||||
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)},
|
||||
count: 34,
|
||||
count: 35,
|
||||
},
|
||||
{
|
||||
name: "AllTemplates",
|
||||
|
|
|
@ -23,6 +23,7 @@ const (
|
|||
GithubEventPullRequestComment = "pull_request_comment"
|
||||
GithubEventGollum = "gollum"
|
||||
GithubEventSchedule = "schedule"
|
||||
GithubEventWorkflowDispatch = "workflow_dispatch"
|
||||
)
|
||||
|
||||
// IsDefaultBranchWorkflow returns true if the event only triggers workflows on the default branch
|
||||
|
@ -52,6 +53,10 @@ func IsDefaultBranchWorkflow(triggedEvent webhook_module.HookEventType) bool {
|
|||
// GitHub "schedule" event
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
|
||||
return true
|
||||
case webhook_module.HookEventWorkflowDispatch:
|
||||
// GitHub "workflow_dispatch" event
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
|
||||
return true
|
||||
case webhook_module.HookEventIssues,
|
||||
webhook_module.HookEventIssueAssign,
|
||||
webhook_module.HookEventIssueLabel,
|
||||
|
@ -74,6 +79,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
|
|||
case GithubEventGollum:
|
||||
return triggedEvent == webhook_module.HookEventWiki
|
||||
|
||||
case GithubEventWorkflowDispatch:
|
||||
return triggedEvent == webhook_module.HookEventWorkflowDispatch
|
||||
|
||||
case GithubEventIssues:
|
||||
switch triggedEvent {
|
||||
case webhook_module.HookEventIssues,
|
||||
|
|
|
@ -191,6 +191,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
|||
|
||||
switch triggedEvent {
|
||||
case // events with no activity types
|
||||
webhook_module.HookEventWorkflowDispatch,
|
||||
webhook_module.HookEventCreate,
|
||||
webhook_module.HookEventDelete,
|
||||
webhook_module.HookEventFork,
|
||||
|
|
|
@ -125,6 +125,13 @@ func TestDetectMatched(t *testing.T) {
|
|||
yamlOn: "on: schedule",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "HookEventWorkflowDispatch(workflow_dispatch) matches GithubEventWorkflowDispatch(workflow_dispatch)",
|
||||
triggedEvent: webhook_module.HookEventWorkflowDispatch,
|
||||
payload: nil,
|
||||
yamlOn: "on: workflow_dispatch",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
|
|
@ -21,10 +21,12 @@ var (
|
|||
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
|
||||
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
|
||||
SkipWorkflowStrings []string `ìni:"SKIP_WORKFLOW_STRINGS"`
|
||||
LimitDispatchInputs int64 `ini:"LIMIT_DISPATCH_INPUTS"`
|
||||
}{
|
||||
Enabled: true,
|
||||
DefaultActionsURL: defaultActionsURLForgejo,
|
||||
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
|
||||
LimitDispatchInputs: 10,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -416,6 +416,14 @@ type SchedulePayload struct {
|
|||
Action HookScheduleAction `json:"action"`
|
||||
}
|
||||
|
||||
type WorkflowDispatchPayload struct {
|
||||
Inputs map[string]string `json:"inputs"`
|
||||
Ref string `json:"ref"`
|
||||
Repository *Repository `json:"repository"`
|
||||
Sender *User `json:"sender"`
|
||||
Workflow string `json:"workflow"`
|
||||
}
|
||||
|
||||
// ReviewPayload FIXME
|
||||
type ReviewPayload struct {
|
||||
Type string `json:"type"`
|
||||
|
|
15
modules/structs/workflow.go
Normal file
15
modules/structs/workflow.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
// Copyright The Forgejo Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
// DispatchWorkflowOption options when dispatching a workflow
|
||||
// swagger:model
|
||||
type DispatchWorkflowOption struct {
|
||||
// Git reference for the workflow
|
||||
//
|
||||
// required: true
|
||||
Ref string `json:"ref"`
|
||||
// Input keys and values configured in the workflow file.
|
||||
Inputs map[string]string `json:"inputs"`
|
||||
}
|
|
@ -32,6 +32,7 @@ const (
|
|||
HookEventRelease HookEventType = "release"
|
||||
HookEventPackage HookEventType = "package"
|
||||
HookEventSchedule HookEventType = "schedule"
|
||||
HookEventWorkflowDispatch HookEventType = "workflow_dispatch"
|
||||
)
|
||||
|
||||
// Event returns the HookEventType as an event string
|
||||
|
|
|
@ -3769,6 +3769,13 @@ workflow.disable_success = Workflow "%s" disabled successfully.
|
|||
workflow.enable = Enable workflow
|
||||
workflow.enable_success = Workflow "%s" enabled successfully.
|
||||
workflow.disabled = Workflow is disabled.
|
||||
workflow.dispatch.trigger_found = This workflow has a <c>workflow_dispatch</c> event trigger.
|
||||
workflow.dispatch.use_from = Use workflow from
|
||||
workflow.dispatch.run = Run workflow
|
||||
workflow.dispatch.success = Workflow run was successfully requested.
|
||||
workflow.dispatch.input_required = Require value for input "%s".
|
||||
workflow.dispatch.invalid_input_type = Invalid input type "%s".
|
||||
workflow.dispatch.warn_input_limit = Only displaying the first %d inputs.
|
||||
|
||||
need_approval_desc = Need approval to run workflows for fork pull request.
|
||||
|
||||
|
|
1
release-notes/8.0.0/3334.md
Normal file
1
release-notes/8.0.0/3334.md
Normal file
|
@ -0,0 +1 @@
|
|||
Added support for the `workflow_dispatch` workflow trigger
|
|
@ -1123,6 +1123,12 @@ func Routes() *web.Route {
|
|||
}, reqToken(), reqAdmin())
|
||||
m.Group("/actions", func() {
|
||||
m.Get("/tasks", repo.ListActionTasks)
|
||||
|
||||
m.Group("/workflows", func() {
|
||||
m.Group("/{workflowname}", func() {
|
||||
m.Post("/dispatches", reqToken(), reqRepoWriter(unit.TypeActions), mustNotBeArchived, bind(api.DispatchWorkflowOption{}), repo.DispatchWorkflow)
|
||||
})
|
||||
})
|
||||
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
|
||||
m.Group("/keys", func() {
|
||||
m.Combo("").Get(repo.ListDeployKeys).
|
||||
|
|
|
@ -583,3 +583,73 @@ func ListActionTasks(ctx *context.APIContext) {
|
|||
|
||||
ctx.JSON(http.StatusOK, &res)
|
||||
}
|
||||
|
||||
// DispatchWorkflow dispatches a workflow
|
||||
func DispatchWorkflow(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflowname}/dispatches repository DispatchWorkflow
|
||||
// ---
|
||||
// summary: Dispatches a workflow
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: workflowname
|
||||
// in: path
|
||||
// description: name of the workflow
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/DispatchWorkflowOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opt := web.GetForm(ctx).(*api.DispatchWorkflowOption)
|
||||
name := ctx.Params("workflowname")
|
||||
|
||||
if len(opt.Ref) == 0 {
|
||||
ctx.Error(http.StatusBadRequest, "ref", nil)
|
||||
return
|
||||
} else if len(name) == 0 {
|
||||
ctx.Error(http.StatusBadRequest, "workflowname", nil)
|
||||
return
|
||||
}
|
||||
|
||||
workflow, err := actions_service.GetWorkflowFromCommit(ctx.Repo.GitRepo, opt.Ref, name)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusNotFound, "GetWorkflowFromCommit", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetWorkflowFromCommit", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
inputGetter := func(key string) string {
|
||||
return opt.Inputs[key]
|
||||
}
|
||||
|
||||
if err := workflow.Dispatch(ctx, inputGetter, ctx.Repo.Repository, ctx.Doer); err != nil {
|
||||
if actions_service.IsInputRequiredErr(err) {
|
||||
ctx.Error(http.StatusBadRequest, "workflow.Dispatch", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "workflow.Dispatch", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusNoContent, nil)
|
||||
}
|
||||
|
|
|
@ -216,4 +216,7 @@ type swaggerParameterBodies struct {
|
|||
|
||||
// in:body
|
||||
UpdateVariableOption api.UpdateVariableOption
|
||||
|
||||
// in:body
|
||||
DispatchWorkflowOption api.DispatchWorkflowOption
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
|
@ -18,6 +19,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
|
@ -59,6 +61,9 @@ func List(ctx *context.Context) {
|
|||
ctx.Data["Title"] = ctx.Tr("actions.actions")
|
||||
ctx.Data["PageIsActions"] = true
|
||||
|
||||
curWorkflow := ctx.FormString("workflow")
|
||||
ctx.Data["CurWorkflow"] = curWorkflow
|
||||
|
||||
var workflows []Workflow
|
||||
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
|
||||
ctx.ServerError("IsEmpty", err)
|
||||
|
@ -140,6 +145,22 @@ func List(ctx *context.Context) {
|
|||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
|
||||
}
|
||||
workflows = append(workflows, workflow)
|
||||
|
||||
if workflow.Entry.Name() == curWorkflow {
|
||||
config := wf.WorkflowDispatchConfig()
|
||||
if config != nil {
|
||||
keys := util.KeysOfMap(config.Inputs)
|
||||
slices.Sort(keys)
|
||||
if int64(len(config.Inputs)) > setting.Actions.LimitDispatchInputs {
|
||||
keys = keys[:setting.Actions.LimitDispatchInputs]
|
||||
}
|
||||
|
||||
ctx.Data["CurWorkflowDispatch"] = config
|
||||
ctx.Data["CurWorkflowDispatchInputKeys"] = keys
|
||||
ctx.Data["WarnDispatchInputsLimit"] = int64(len(config.Inputs)) > setting.Actions.LimitDispatchInputs
|
||||
ctx.Data["DispatchInputsLimit"] = setting.Actions.LimitDispatchInputs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Data["workflows"] = workflows
|
||||
|
@ -150,17 +171,15 @@ func List(ctx *context.Context) {
|
|||
page = 1
|
||||
}
|
||||
|
||||
workflow := ctx.FormString("workflow")
|
||||
actorID := ctx.FormInt64("actor")
|
||||
status := ctx.FormInt("status")
|
||||
ctx.Data["CurWorkflow"] = workflow
|
||||
|
||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
ctx.Data["ActionsConfig"] = actionsConfig
|
||||
|
||||
if len(workflow) > 0 && ctx.Repo.IsAdmin() {
|
||||
if len(curWorkflow) > 0 && ctx.Repo.IsAdmin() {
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = true
|
||||
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(workflow)
|
||||
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(curWorkflow)
|
||||
}
|
||||
|
||||
// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
|
||||
|
@ -177,7 +196,7 @@ func List(ctx *context.Context) {
|
|||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||
},
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
WorkflowID: workflow,
|
||||
WorkflowID: curWorkflow,
|
||||
TriggerUserID: actorID,
|
||||
}
|
||||
|
||||
|
@ -203,6 +222,8 @@ func List(ctx *context.Context) {
|
|||
|
||||
ctx.Data["Runs"] = runs
|
||||
|
||||
ctx.Data["Repo"] = ctx.Repo
|
||||
|
||||
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetActors", err)
|
||||
|
@ -214,7 +235,7 @@ func List(ctx *context.Context) {
|
|||
|
||||
pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
pager.AddParamString("workflow", workflow)
|
||||
pager.AddParamString("workflow", curWorkflow)
|
||||
pager.AddParamString("actor", fmt.Sprint(actorID))
|
||||
pager.AddParamString("status", fmt.Sprint(status))
|
||||
ctx.Data["Page"] = pager
|
||||
|
|
62
routers/web/repo/actions/manual.go
Normal file
62
routers/web/repo/actions/manual.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright The Forgejo Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
context_module "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func ManualRunWorkflow(ctx *context_module.Context) {
|
||||
workflowID := ctx.FormString("workflow")
|
||||
if len(workflowID) == 0 {
|
||||
ctx.ServerError("workflow", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ref := ctx.FormString("ref")
|
||||
if len(ref) == 0 {
|
||||
ctx.ServerError("ref", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
|
||||
ctx.ServerError("IsEmpty", err)
|
||||
return
|
||||
} else if empty {
|
||||
ctx.NotFound("IsEmpty", nil)
|
||||
return
|
||||
}
|
||||
|
||||
workflow, err := actions_service.GetWorkflowFromCommit(ctx.Repo.GitRepo, ref, workflowID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWorkflowFromCommit", err)
|
||||
return
|
||||
}
|
||||
|
||||
location := ctx.Repo.RepoLink + "/actions?workflow=" + url.QueryEscape(workflowID) +
|
||||
"&actor=" + url.QueryEscape(ctx.FormString("actor")) +
|
||||
"&status=" + url.QueryEscape(ctx.FormString("status"))
|
||||
|
||||
formKeyGetter := func(key string) string {
|
||||
formKey := "inputs[" + key + "]"
|
||||
return ctx.FormString(formKey)
|
||||
}
|
||||
|
||||
if err := workflow.Dispatch(ctx, formKeyGetter, ctx.Repo.Repository, ctx.Doer); err != nil {
|
||||
if actions_service.IsInputRequiredErr(err) {
|
||||
ctx.Flash.Error(ctx.Locale.Tr("actions.workflow.dispatch.input_required", err.(actions_service.InputRequiredErr).Name))
|
||||
ctx.Redirect(location)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("workflow.Dispatch", err)
|
||||
return
|
||||
}
|
||||
|
||||
// forward to the page of the run which was just created
|
||||
ctx.Flash.Info(ctx.Locale.Tr("actions.workflow.dispatch.success"))
|
||||
ctx.Redirect(location)
|
||||
}
|
|
@ -1376,6 +1376,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Get("", actions.List)
|
||||
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
|
||||
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
|
||||
m.Post("/manual", reqRepoAdmin, actions.ManualRunWorkflow)
|
||||
|
||||
m.Group("/runs", func() {
|
||||
m.Get("/latest", actions.ViewLatest)
|
||||
|
|
171
services/actions/workflows.go
Normal file
171
services/actions/workflows.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
// Copyright The Forgejo Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/webhook"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
|
||||
"github.com/nektos/act/pkg/jobparser"
|
||||
act_model "github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
type InputRequiredErr struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (err InputRequiredErr) Error() string {
|
||||
return fmt.Sprintf("input required for '%s'", err.Name)
|
||||
}
|
||||
|
||||
func IsInputRequiredErr(err error) bool {
|
||||
_, ok := err.(InputRequiredErr)
|
||||
return ok
|
||||
}
|
||||
|
||||
type Workflow struct {
|
||||
WorkflowID string
|
||||
Ref string
|
||||
Commit *git.Commit
|
||||
GitEntry *git.TreeEntry
|
||||
}
|
||||
|
||||
type InputValueGetter func(key string) string
|
||||
|
||||
func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGetter, repo *repo_model.Repository, doer *user.User) error {
|
||||
content, err := actions.GetContentFromEntry(entry.GitEntry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullWorkflowID := ".forgejo/workflows/" + entry.WorkflowID
|
||||
|
||||
title := wf.Name
|
||||
if len(title) < 1 {
|
||||
title = fullWorkflowID
|
||||
}
|
||||
|
||||
inputs := make(map[string]string)
|
||||
if workflowDispatch := wf.WorkflowDispatchConfig(); workflowDispatch != nil {
|
||||
for key, input := range workflowDispatch.Inputs {
|
||||
val := inputGetter(key)
|
||||
if len(val) == 0 {
|
||||
val = input.Default
|
||||
if len(val) == 0 {
|
||||
if input.Required {
|
||||
name := input.Description
|
||||
if len(name) == 0 {
|
||||
name = key
|
||||
}
|
||||
return InputRequiredErr{Name: name}
|
||||
}
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
switch input.Type {
|
||||
case "boolean":
|
||||
// Since "boolean" inputs are rendered as a checkbox in html, the value inside the form is "on"
|
||||
val = strconv.FormatBool(val == "on")
|
||||
}
|
||||
}
|
||||
inputs[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
if int64(len(inputs)) > setting.Actions.LimitDispatchInputs {
|
||||
return errors.New("to many inputs")
|
||||
}
|
||||
|
||||
payload := &structs.WorkflowDispatchPayload{
|
||||
Inputs: inputs,
|
||||
Ref: entry.Ref,
|
||||
Repository: convert.ToRepo(ctx, repo, access.Permission{AccessMode: perm.AccessModeNone}),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
Workflow: fullWorkflowID,
|
||||
}
|
||||
|
||||
p, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
run := &actions_model.ActionRun{
|
||||
Title: title,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
OwnerID: repo.OwnerID,
|
||||
WorkflowID: entry.WorkflowID,
|
||||
TriggerUserID: doer.ID,
|
||||
TriggerUser: doer,
|
||||
Ref: entry.Ref,
|
||||
CommitSHA: entry.Commit.ID.String(),
|
||||
Event: webhook.HookEventWorkflowDispatch,
|
||||
EventPayload: string(p),
|
||||
TriggerEvent: string(webhook.HookEventWorkflowDispatch),
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
|
||||
vars, err := actions_model.GetVariablesOfRun(ctx, run)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jobs, err := jobparser.Parse(content, jobparser.WithVars(vars))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return actions_model.InsertRun(ctx, run, jobs)
|
||||
}
|
||||
|
||||
func GetWorkflowFromCommit(gitRepo *git.Repository, ref, workflowID string) (*Workflow, error) {
|
||||
commit, err := gitRepo.GetCommit(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := actions.ListWorkflows(commit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var workflowEntry *git.TreeEntry
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == workflowID {
|
||||
workflowEntry = entry
|
||||
break
|
||||
}
|
||||
}
|
||||
if workflowEntry == nil {
|
||||
return nil, errors.New("workflow not found")
|
||||
}
|
||||
|
||||
return &Workflow{
|
||||
WorkflowID: workflowID,
|
||||
Ref: ref,
|
||||
Commit: commit,
|
||||
GitEntry: workflowEntry,
|
||||
}, nil
|
||||
}
|
99
templates/repo/actions/dispatch.tmpl
Normal file
99
templates/repo/actions/dispatch.tmpl
Normal file
|
@ -0,0 +1,99 @@
|
|||
<div class="ui info message tw-flex tw-items-center">
|
||||
<span>
|
||||
{{ctx.Locale.Tr "actions.workflow.dispatch.trigger_found"}}
|
||||
</span>
|
||||
<div class="ui dropdown custom tw-ml-4" id="workflow_dispatch_dropdown">
|
||||
<button class="ui compact small basic button tw-flex">
|
||||
<span class="text">{{ctx.Locale.Tr "actions.workflow.dispatch.run"}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</button>
|
||||
<div class="menu">
|
||||
<div class="message ui form">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "actions.workflow.dispatch.use_from"}}</label>
|
||||
{{template "repo/branch_dropdown" dict
|
||||
"root" (dict
|
||||
"IsViewBranch" true
|
||||
"BranchName" .Repo.BranchName
|
||||
"CommitID" .Repo.CommitID
|
||||
"RepoLink" .Repo.RepoLink
|
||||
"Repository" .Repo.Repository
|
||||
)
|
||||
"disableCreateBranch" true
|
||||
"branchForm" "branch-dropdown-form"
|
||||
"setAction" false
|
||||
"submitForm" false
|
||||
}}
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{.Repo.RepoLink}}/actions/manual" id="branch-dropdown-form">
|
||||
{{range $i, $key := .CurWorkflowDispatchInputKeys}}
|
||||
{{$val := index $.CurWorkflowDispatch.Inputs $key}}
|
||||
<div class="{{if $val.Required}}required {{end}}field">
|
||||
{{if eq $val.Type "boolean"}}
|
||||
<div class="ui checkbox">
|
||||
<label><strong>{{if $val.Description}}{{$val.Description}}{{else}}{{$key}}{{end}}</strong></label>
|
||||
<input {{if $val.Required}}required{{end}} type="checkbox" name="inputs[{{$key}}]" {{if eq $val.Default "true"}}checked{{end}}>
|
||||
</div>
|
||||
{{else}}
|
||||
<label>{{if $val.Description}}{{$val.Description}}{{else}}{{$key}}{{end}}</label>
|
||||
{{if eq $val.Type "number"}}
|
||||
<input {{if $val.Required}}required{{end}} type="number" name="inputs[{{$key}}]" {{if $val.Default}}value="{{$val.Default}}"{{end}}>
|
||||
{{else if eq $val.Type "string"}}
|
||||
<input {{if $val.Required}}required{{end}} type="text" name="inputs[{{$key}}]" {{if $val.Default}}value="{{$val.Default}}"{{end}}>
|
||||
{{else if eq $val.Type "choice"}}
|
||||
<div class="ui selection dropdown">
|
||||
<input name="inputs[{{$key}}]" type="hidden" value="{{$val.Default}}">
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="text"></div>
|
||||
<div class="menu">
|
||||
{{range $opt := $val.Options}}
|
||||
<div data-value="{{$opt}}" class="{{if eq $val.Default $opt}}active selected {{end}}item">{{$opt}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<strong>{{ctx.Locale.Tr "actions.workflow.dispatch.invalid_input_type" $val.Type}}</strong>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .WarnDispatchInputsLimit}}
|
||||
<div class="text yellow tw-mb-4">
|
||||
{{svg "octicon-alert"}} {{ctx.Locale.Tr "actions.workflow.dispatch.warn_input_limit" .DispatchInputsLimit}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="ref" value="{{if $.Repo.BranchName}}{{$.Repo.BranchName}}{{else}}{{$.Repo.Repository.DefaultBranch}}{{end}}">
|
||||
<input type="hidden" name="workflow" value="{{$.CurWorkflow}}">
|
||||
<input type="hidden" name="actor" value="{{$.CurActor}}">
|
||||
<input type="hidden" name="status" value="{{$.CurStatus}}">
|
||||
<button type="submit" id="workflow-dispatch-submit" class="ui primary small compact button">{{ctx.Locale.Tr "actions.workflow.dispatch.run"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
const dropdown = $('#workflow_dispatch_dropdown');
|
||||
const menu = dropdown.find('> .menu');
|
||||
$(document.body).on('click', (ev) => {
|
||||
if (!dropdown[0].contains(ev.target) && menu.hasClass('visible')) {
|
||||
menu.transition({ animation: 'slide down out', duration: 200, queue: false });
|
||||
}
|
||||
});
|
||||
dropdown.on('click', (ev) => {
|
||||
const inMenu = $(ev.target).closest(menu).length !== 0;
|
||||
if (inMenu) return;
|
||||
ev.stopPropagation();
|
||||
if (menu.hasClass('visible')) {
|
||||
menu.transition({ animation: 'slide down out', duration: 200, queue: false });
|
||||
} else {
|
||||
menu.transition({ animation: 'slide down in', duration: 200, queue: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
|
@ -76,6 +76,11 @@
|
|||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if $.CurWorkflowDispatch}}
|
||||
{{template "repo/actions/dispatch" .}}
|
||||
{{end}}
|
||||
|
||||
{{template "repo/actions/runs_list" .}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
75
templates/swagger/v1_json.tmpl
generated
75
templates/swagger/v1_json.tmpl
generated
|
@ -4239,6 +4239,56 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/workflows/{workflowname}/dispatches": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Dispatches a workflow",
|
||||
"operationId": "DispatchWorkflow",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the workflow",
|
||||
"name": "workflowname",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DispatchWorkflowOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/activities/feeds": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -20902,6 +20952,29 @@
|
|||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"DispatchWorkflowOption": {
|
||||
"description": "DispatchWorkflowOption options when dispatching a workflow",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ref"
|
||||
],
|
||||
"properties": {
|
||||
"inputs": {
|
||||
"description": "Input keys and values configured in the workflow file.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "Inputs"
|
||||
},
|
||||
"ref": {
|
||||
"description": "Git reference for the workflow",
|
||||
"type": "string",
|
||||
"x-go-name": "Ref"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"EditAttachmentOptions": {
|
||||
"description": "EditAttachmentOptions options for editing attachments",
|
||||
"type": "object",
|
||||
|
@ -26627,7 +26700,7 @@
|
|||
"parameterBodies": {
|
||||
"description": "parameterBodies",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/UpdateVariableOption"
|
||||
"$ref": "#/definitions/DispatchWorkflowOption"
|
||||
}
|
||||
},
|
||||
"redirect": {
|
||||
|
|
74
tests/e2e/actions.test.e2e.js
Normal file
74
tests/e2e/actions.test.e2e.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
// @ts-check
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login_user, load_logged_in_context} from './utils_e2e.js';
|
||||
|
||||
test.beforeAll(async ({browser}, workerInfo) => {
|
||||
await login_user(browser, workerInfo, 'user2');
|
||||
});
|
||||
|
||||
test('Test workflow dispatch present', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
/** @type {import('@playwright/test').Page} */
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
|
||||
|
||||
await expect(page.getByText('This workflow has a workflow_dispatch event trigger.')).toBeVisible();
|
||||
|
||||
const run_workflow_btn = page.locator('#workflow_dispatch_dropdown>button');
|
||||
await expect(run_workflow_btn).toBeVisible();
|
||||
|
||||
const menu = page.locator('#workflow_dispatch_dropdown>.menu');
|
||||
await expect(menu).toBeHidden();
|
||||
await run_workflow_btn.click();
|
||||
await expect(menu).toBeVisible();
|
||||
});
|
||||
|
||||
test('Test workflow dispatch error: missing inputs', async ({browser}, workerInfo) => {
|
||||
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
|
||||
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
/** @type {import('@playwright/test').Page} */
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.locator('#workflow_dispatch_dropdown>button').click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Remove the required attribute so we can trigger the error message!
|
||||
await page.evaluate(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
const elem = document.querySelector('input[name="inputs[string2]"]');
|
||||
elem?.removeAttribute('required');
|
||||
});
|
||||
|
||||
await page.locator('#workflow-dispatch-submit').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Test workflow dispatch success', async ({browser}, workerInfo) => {
|
||||
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
|
||||
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
/** @type {import('@playwright/test').Page} */
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.locator('#workflow_dispatch_dropdown>button').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.type('input[name="inputs[string2]"]', 'abc');
|
||||
await page.locator('#workflow-dispatch-submit').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible();
|
||||
|
||||
await expect(page.locator('.run-list>:first-child .run-list-meta', {hasText: 'now'})).toBeVisible();
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/main
|
|
@ -0,0 +1,4 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
# pack-refs with: peeled fully-peeled sorted
|
||||
774f93df12d14931ea93259ae93418da4482fcc1 refs/heads/main
|
||||
774f93df12d14931ea93259ae93418da4482fcc1 refs/heads/master
|
|
@ -396,3 +396,46 @@ func TestCreateDeleteRefEvent(t *testing.T) {
|
|||
assert.NotNil(t, run)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkflowDispatchEvent(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// create the repo
|
||||
repo, sha, f := CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch",
|
||||
[]unit_model.Type{unit_model.TypeActions}, nil,
|
||||
[]*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: ".gitea/workflows/dispatch.yml",
|
||||
ContentReader: strings.NewReader(
|
||||
"name: test\n" +
|
||||
"on: [workflow_dispatch]\n" +
|
||||
"jobs:\n" +
|
||||
" test:\n" +
|
||||
" runs-on: ubuntu-latest\n" +
|
||||
" steps:\n" +
|
||||
" - run: echo helloworld\n",
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
defer f()
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
workflow, err := actions_service.GetWorkflowFromCommit(gitRepo, sha, "dispatch.yml")
|
||||
assert.NoError(t, err)
|
||||
|
||||
inputGetter := func(key string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -95,9 +95,9 @@ func TestAPISearchRepo(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{
|
||||
nil: {count: 36},
|
||||
user: {count: 36},
|
||||
user2: {count: 36},
|
||||
nil: {count: 37},
|
||||
user: {count: 37},
|
||||
user2: {count: 37},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -81,3 +81,16 @@
|
|||
max-width: 110px;
|
||||
}
|
||||
}
|
||||
|
||||
#workflow_dispatch_dropdown {
|
||||
min-width: min-content;
|
||||
}
|
||||
#workflow_dispatch_dropdown > button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 640px) or (767.98px < width < 854px) {
|
||||
#workflow_dispatch_dropdown .menu {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue