Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Add event, schedule and manual workflow triggers

eb07ff5696e0d83201215966e3ea1e6a24467581 · Keenan Nemetz · 8d ago · parent 1cde8b9e

Add event, schedule and manual workflow triggers

Workflows can now react to change requests, issues, comments, cron
schedules and manual runs, and a push trigger can filter by the paths it
changed. Branch and tag patterns gain ! exclusion.

Event runs are pinned with a JSON payload and the runner exposes it as
VOLE_EVENT, VOLE_EVENT_ACTION and VOLE_EVENT_PATH. Change-request runs
check out the base tip and carry the head and changed files, so an event
never executes an unapproved contributor commit.

Workflow dispatch resolves typed inputs and is available from the
workflows page and `vole workflow dispatch`. Deduplication now includes
an occurrence so the same commit can run for a later event or schedule
slot while a push remains idempotent.

Part of: #138

server/web/workflow_dispatch.go +156

@@ -0,0 +1,156 @@
1 package web
2
3 import (
4 "net/http"
5 "sort"
6 "strings"
7
8 "github.com/rodent-software/vole/ci"
9 )
10
11 type dispatchInputView struct {
12 Name string
13 Description string
14 Type string
15 Required bool
16 Default string
17 Options []string
18 }
19
20 type dispatchView struct {
21 Page
22 Name string
23 Workflow *ci.Workflow
24 Ref string
25 Branches []string
26 Inputs []dispatchInputView
27 Error string
28 }
29
30 // WorkflowDispatchPage shows the form for one workflow that declared
31 // workflow-dispatch. It is a maintainer surface: the run executes repository code.
32 func (w *Web) WorkflowDispatchPage(rw http.ResponseWriter, r *http.Request) {
33 m, ok := w.open(rw, r)
34 if !ok {
35 return
36 }
37 if !w.canDispatch(rw, r, m.Name) {
38 return
39 }
40 w.renderDispatch(rw, r, m.Name, r.URL.Query().Get("workflow"), r.URL.Query().Get("ref"), "")
41 }
42
43 // DispatchWorkflow queues a manual run from the browser form.
44 func (w *Web) WorkflowDispatch(rw http.ResponseWriter, r *http.Request) {
45 ctx, ok := w.beginWrite(rw, r, maxFormBody)
46 if !ok {
47 return
48 }
49 m := ctx.Repo
50 if !w.canDispatch(rw, r, m.Name) {
51 return
52 }
53 workflow := strings.TrimSpace(r.PostFormValue("workflow"))
54 ref := strings.TrimSpace(r.PostFormValue("ref"))
55 inputs := map[string]string{}
56 for name, values := range r.PostForm {
57 switch name {
58 case "csrf", "workflow", "ref":
59 continue
60 }
61 if len(values) > 0 {
62 inputs[name] = values[0]
63 }
64 }
65 if w.DispatchWorkflow == nil {
66 w.serverError(rw, r, "manual runs are not configured on this server")
67 return
68 }
69 if _, err := w.DispatchWorkflow(m.Name, workflow, ref, inputs, ctx.Name); err != nil {
70 w.renderDispatch(rw, r, m.Name, workflow, ref, err.Error())
71 return
72 }
73 w.audit("web.workflow.dispatch", ctx.Name, m.Name, workflow+" "+ref)
74 http.Redirect(rw, r, "/r/"+m.Name+"/runs", http.StatusSeeOther)
75 }
76
77 func (w *Web) canDispatch(rw http.ResponseWriter, r *http.Request, repoName string) bool {
78 m, err := w.Repos.Open(repoName)
79 if err != nil {
80 w.notFound(rw, r)
81 return false
82 }
83 pr := w.principal(r)
84 if pr == nil {
85 w.notFound(rw, r)
86 return false
87 }
88 acl, err := m.ACL()
89 if err != nil || acl == nil {
90 w.notFound(rw, r)
91 return false
92 }
93 if !canMaintain(acl, pr.account.Name) &&
94 !(w.Ident != nil && w.Ident.IsAdmin(pr.account.Name)) {
95 w.notFound(rw, r)
96 return false
97 }
98 return true
99 }
100
101 func (w *Web) renderDispatch(rw http.ResponseWriter, r *http.Request, repoName, workflowPath, ref, message string) {
102 m, err := w.Repos.Open(repoName)
103 if err != nil {
104 w.notFound(rw, r)
105 return
106 }
107 commit, _ := m.Git().RevParse(m.DefaultBranch)
108 wfs, problems := ci.At(m.Git(), commit)
109 if len(problems) > 0 {
110 w.render(rw, "workflow_dispatch", dispatchView{
111 Page: w.page(r, m.Name+" · run workflow"), Name: m.Name, Error: strings.Join(problems, "; "),
112 }, 0)
113 return
114 }
115 var wf *ci.Workflow
116 for _, candidate := range wfs {
117 if candidate.Path == workflowPath {
118 wf = candidate
119 break
120 }
121 }
122 if wf == nil {
123 w.notFound(rw, r)
124 return
125 }
126 if wf.Dispatch == nil {
127 http.Error(rw, "this workflow does not declare workflow-dispatch", http.StatusBadRequest)
128 return
129 }
130 if strings.TrimSpace(ref) == "" {
131 ref = m.DefaultBranch
132 }
133 refs, _ := m.Git().ListRefs("refs/heads/")
134 branches := make([]string, 0, len(refs))
135 for name := range refs {
136 branches = append(branches, strings.TrimPrefix(name, "refs/heads/"))
137 }
138 sort.Strings(branches)
139 names := make([]string, 0, len(wf.Dispatch.Inputs))
140 for name := range wf.Dispatch.Inputs {
141 names = append(names, name)
142 }
143 sort.Strings(names)
144 inputs := make([]dispatchInputView, 0, len(names))
145 for _, name := range names {
146 in := wf.Dispatch.Inputs[name]
147 inputs = append(inputs, dispatchInputView{
148 Name: name, Description: in.Description, Type: in.Type,
149 Required: in.Required, Default: in.Default, Options: in.Options,
150 })
151 }
152 w.render(rw, "workflow_dispatch", dispatchView{
153 Page: w.page(r, m.Name+" · run workflow"), Name: m.Name, Workflow: wf,
154 Ref: ref, Branches: branches, Inputs: inputs, Error: message,
155 }, 0)
156 }