Skip to content

Commit 36232b6

Browse files
pangliangTKaxv-7Ssilverwinddenyskonlunny
authored
Actions support workflow dispatch event (#28163)
fix #23668 My plan: * In the `actions.list` method, if workflow is selected and IsAdmin, check whether the on event contains `workflow_dispatch`. If so, display a `Run workflow` button to allow the user to manually trigger the run. * Providing a form that allows users to select target brach or tag, and these parameters can be configured in yaml * Simple form validation, `required` input cannot be empty * Add a route `/actions/run`, and an `actions.Run` method to handle * Add `WorkflowDispatchPayload` struct to pass the Webhook event payload to the runner when triggered, this payload carries the `inputs` values and other fields, doc: [workflow_dispatch payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch) Other PRs * the `Workflow.WorkflowDispatchConfig()` method still return non-nil when workflow_dispatch is not defined. I submitted a PR https://gitea.com/gitea/act/pulls/85 to fix it. Still waiting for them to process. Behavior should be same with github, but may cause confusion. Here's a quick reminder. * [Doc](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch) Said: This event will `only` trigger a workflow run if the workflow file is `on the default branch`. * If the workflow yaml file only exists in a non-default branch, it cannot be triggered. (It will not even show up in the workflow list) * If the same workflow yaml file exists in each branch at the same time, the version of the default branch is used. Even if `Use workflow from` selects another branch ![image](https://github.com/go-gitea/gitea/assets/3114995/4bf596f3-426b-48e8-9b8f-0f6d18defd79) ```yaml name: Docker Image CI 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 environment: description: 'Environment to run tests against' type: environment required: true default: 'environment values' number_required_1: description: 'number ' type: number required: true default: '100' number_required_2: description: 'number' type: number required: true default: '100' number_required_3: description: 'number' type: number required: true default: '100' number_1: description: 'number' type: number required: false number_2: description: 'number' type: number required: false number_3: description: 'number' type: number required: false env: inputs_logLevel: ${{ inputs.logLevel }} inputs_tags: ${{ inputs.tags }} inputs_boolean_default_true: ${{ inputs.boolean_default_true }} inputs_boolean_default_false: ${{ inputs.boolean_default_false }} inputs_environment: ${{ inputs.environment }} inputs_number_1: ${{ inputs.number_1 }} inputs_number_2: ${{ inputs.number_2 }} inputs_number_3: ${{ inputs.number_3 }} inputs_number_required_1: ${{ inputs.number_required_1 }} inputs_number_required_2: ${{ inputs.number_required_2 }} inputs_number_required_3: ${{ inputs.number_required_3 }} jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: ls -la - run: env | grep inputs - run: echo ${{ inputs.logLevel }} - run: echo ${{ inputs.boolean_default_false }} ``` ![image](https://github.com/go-gitea/gitea/assets/3114995/a58a842d-a0ff-4618-bc6d-83a9596d07c8) ![image](https://github.com/go-gitea/gitea/assets/3114995/44a7cca5-7bd4-42a9-8723-91751a501c88) --------- Co-authored-by: TKaxv_7S <954067342@qq.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Denys Konovalov <kontakt@denyskon.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
1 parent 561b5c5 commit 36232b6

File tree

10 files changed

+580
-17
lines changed

10 files changed

+580
-17
lines changed

Diff for: modules/structs/hook.go

+14
Original file line numberDiff line numberDiff line change
@@ -494,3 +494,17 @@ type PackagePayload struct {
494494
func (p *PackagePayload) JSONPayload() ([]byte, error) {
495495
return json.MarshalIndent(p, "", " ")
496496
}
497+
498+
// WorkflowDispatchPayload represents a workflow dispatch payload
499+
type WorkflowDispatchPayload struct {
500+
Workflow string `json:"workflow"`
501+
Ref string `json:"ref"`
502+
Inputs map[string]any `json:"inputs"`
503+
Repository *Repository `json:"repository"`
504+
Sender *User `json:"sender"`
505+
}
506+
507+
// JSONPayload implements Payload
508+
func (p *WorkflowDispatchPayload) JSONPayload() ([]byte, error) {
509+
return json.MarshalIndent(p, "", " ")
510+
}

Diff for: options/locale/locale_en-US.ini

+6
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,7 @@ org_still_own_repo = "This organization still owns one or more repositories, del
628628
org_still_own_packages = "This organization still owns one or more packages, delete them first."
629629
630630
target_branch_not_exist = Target branch does not exist.
631+
target_ref_not_exist = Target ref does not exist %s
631632
632633
admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first.
633634
@@ -3701,6 +3702,11 @@ workflow.disable_success = Workflow '%s' disabled successfully.
37013702
workflow.enable = Enable Workflow
37023703
workflow.enable_success = Workflow '%s' enabled successfully.
37033704
workflow.disabled = Workflow is disabled.
3705+
workflow.run = Run Workflow
3706+
workflow.not_found = Workflow '%s' not found.
3707+
workflow.run_success = Workflow '%s' run successfully.
3708+
workflow.from_ref = Use workflow from
3709+
workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger.
37043710

37053711
need_approval_desc = Need approval to run workflows for fork pull request.
37063712

Diff for: routers/web/repo/actions/actions.go

+136-9
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,28 @@ import (
77
"bytes"
88
"fmt"
99
"net/http"
10+
"slices"
1011
"strings"
1112

1213
actions_model "code.gitea.io/gitea/models/actions"
1314
"code.gitea.io/gitea/models/db"
15+
git_model "code.gitea.io/gitea/models/git"
16+
repo_model "code.gitea.io/gitea/models/repo"
1417
"code.gitea.io/gitea/models/unit"
1518
"code.gitea.io/gitea/modules/actions"
1619
"code.gitea.io/gitea/modules/base"
1720
"code.gitea.io/gitea/modules/container"
1821
"code.gitea.io/gitea/modules/git"
22+
"code.gitea.io/gitea/modules/log"
1923
"code.gitea.io/gitea/modules/optional"
2024
"code.gitea.io/gitea/modules/setting"
25+
"code.gitea.io/gitea/modules/util"
2126
"code.gitea.io/gitea/routers/web/repo"
2227
"code.gitea.io/gitea/services/context"
2328
"code.gitea.io/gitea/services/convert"
2429

2530
"github.com/nektos/act/pkg/model"
31+
"gopkg.in/yaml.v3"
2632
)
2733

2834
const (
@@ -58,8 +64,13 @@ func MustEnableActions(ctx *context.Context) {
5864
func List(ctx *context.Context) {
5965
ctx.Data["Title"] = ctx.Tr("actions.actions")
6066
ctx.Data["PageIsActions"] = true
67+
workflowID := ctx.FormString("workflow")
68+
actorID := ctx.FormInt64("actor")
69+
status := ctx.FormInt("status")
70+
ctx.Data["CurWorkflow"] = workflowID
6171

6272
var workflows []Workflow
73+
var curWorkflow *model.Workflow
6374
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
6475
ctx.ServerError("IsEmpty", err)
6576
return
@@ -140,6 +151,10 @@ func List(ctx *context.Context) {
140151
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
141152
}
142153
workflows = append(workflows, workflow)
154+
155+
if workflow.Entry.Name() == workflowID {
156+
curWorkflow = wf
157+
}
143158
}
144159
}
145160
ctx.Data["workflows"] = workflows
@@ -150,17 +165,46 @@ func List(ctx *context.Context) {
150165
page = 1
151166
}
152167

153-
workflow := ctx.FormString("workflow")
154-
actorID := ctx.FormInt64("actor")
155-
status := ctx.FormInt("status")
156-
ctx.Data["CurWorkflow"] = workflow
157-
158168
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
159169
ctx.Data["ActionsConfig"] = actionsConfig
160170

161-
if len(workflow) > 0 && ctx.Repo.IsAdmin() {
171+
if len(workflowID) > 0 && ctx.Repo.IsAdmin() {
162172
ctx.Data["AllowDisableOrEnableWorkflow"] = true
163-
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(workflow)
173+
isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID)
174+
ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled
175+
176+
if !isWorkflowDisabled && curWorkflow != nil {
177+
workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
178+
if workflowDispatchConfig != nil {
179+
ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig
180+
181+
branchOpts := git_model.FindBranchOptions{
182+
RepoID: ctx.Repo.Repository.ID,
183+
IsDeletedBranch: optional.Some(false),
184+
ListOptions: db.ListOptions{
185+
ListAll: true,
186+
},
187+
}
188+
branches, err := git_model.FindBranchNames(ctx, branchOpts)
189+
if err != nil {
190+
ctx.ServerError("FindBranchNames", err)
191+
return
192+
}
193+
// always put default branch on the top if it exists
194+
if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
195+
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
196+
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
197+
}
198+
ctx.Data["Branches"] = branches
199+
200+
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
201+
if err != nil {
202+
ctx.ServerError("GetTagNamesByRepoID", err)
203+
return
204+
}
205+
ctx.Data["Tags"] = tags
206+
}
207+
}
164208
}
165209

166210
// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
@@ -177,7 +221,7 @@ func List(ctx *context.Context) {
177221
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
178222
},
179223
RepoID: ctx.Repo.Repository.ID,
180-
WorkflowID: workflow,
224+
WorkflowID: workflowID,
181225
TriggerUserID: actorID,
182226
}
183227

@@ -214,11 +258,94 @@ func List(ctx *context.Context) {
214258

215259
pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
216260
pager.SetDefaultParams(ctx)
217-
pager.AddParamString("workflow", workflow)
261+
pager.AddParamString("workflow", workflowID)
218262
pager.AddParamString("actor", fmt.Sprint(actorID))
219263
pager.AddParamString("status", fmt.Sprint(status))
220264
ctx.Data["Page"] = pager
221265
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
222266

223267
ctx.HTML(http.StatusOK, tplListActions)
224268
}
269+
270+
type WorkflowDispatchInput struct {
271+
Name string `yaml:"name"`
272+
Description string `yaml:"description"`
273+
Required bool `yaml:"required"`
274+
Default string `yaml:"default"`
275+
Type string `yaml:"type"`
276+
Options []string `yaml:"options"`
277+
}
278+
279+
type WorkflowDispatch struct {
280+
Inputs []WorkflowDispatchInput
281+
}
282+
283+
func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
284+
switch w.RawOn.Kind {
285+
case yaml.ScalarNode:
286+
var val string
287+
if !decodeNode(w.RawOn, &val) {
288+
return nil
289+
}
290+
if val == "workflow_dispatch" {
291+
return &WorkflowDispatch{}
292+
}
293+
case yaml.SequenceNode:
294+
var val []string
295+
if !decodeNode(w.RawOn, &val) {
296+
return nil
297+
}
298+
for _, v := range val {
299+
if v == "workflow_dispatch" {
300+
return &WorkflowDispatch{}
301+
}
302+
}
303+
case yaml.MappingNode:
304+
var val map[string]yaml.Node
305+
if !decodeNode(w.RawOn, &val) {
306+
return nil
307+
}
308+
309+
workflowDispatchNode, found := val["workflow_dispatch"]
310+
if !found {
311+
return nil
312+
}
313+
314+
var workflowDispatch WorkflowDispatch
315+
var workflowDispatchVal map[string]yaml.Node
316+
if !decodeNode(workflowDispatchNode, &workflowDispatchVal) {
317+
return &workflowDispatch
318+
}
319+
320+
inputsNode, found := workflowDispatchVal["inputs"]
321+
if !found || inputsNode.Kind != yaml.MappingNode {
322+
return &workflowDispatch
323+
}
324+
325+
i := 0
326+
for {
327+
if i+1 >= len(inputsNode.Content) {
328+
break
329+
}
330+
var input WorkflowDispatchInput
331+
if decodeNode(*inputsNode.Content[i+1], &input) {
332+
input.Name = inputsNode.Content[i].Value
333+
workflowDispatch.Inputs = append(workflowDispatch.Inputs, input)
334+
}
335+
i += 2
336+
}
337+
return &workflowDispatch
338+
339+
default:
340+
return nil
341+
}
342+
return nil
343+
}
344+
345+
func decodeNode(node yaml.Node, out any) bool {
346+
if err := node.Decode(out); err != nil {
347+
log.Warn("Failed to decode node %v into %T: %v", node, out, err)
348+
return false
349+
}
350+
return true
351+
}

0 commit comments

Comments
 (0)