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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

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

Add the workflow list and run cancel/rerun actions

0b26547e3ead573cfa604260d5f0bde83bcf18b0 · Keenan Nemetz · 15d ago · parent 455835e7

Add the workflow list and run cancel/rerun actions

The Runs tab now links to /r/{name}/workflows, which reads the default branch with the same validator the push boundary uses and shows triggers, jobs, runner labels, image, steps, outputs, and each job's latest run.

Maintainers can cancel a queued run or rerun a finished one. Rerun is another attempt on the same queue row, not a second row: the queue deliberately has one run per (repo, ref, sha, workflow, job), and attempts already carry their own logs and attestations. Running jobs are not interrupted. Both actions carry CSRF and are audited.

server/web/run_actions.go +83

@@ -0,0 +1,83 @@
1 package web
2
3 // CI actions on one run: cancel a queued job, or enqueue the same pinned job
4 // again. A running job is not interrupted; the queue does not own the process.
5
6 import (
7 "net/http"
8 "time"
9
10 "github.com/rodent-software/vole/ci/runs"
11 "github.com/rodent-software/vole/forge/repo"
12 )
13
14 func (w *Web) runAction(rw http.ResponseWriter, r *http.Request) (*repo.Meta, *runs.Run, string, bool) {
15 pr, ok := w.requireHTMLUser(rw, r)
16 if !ok {
17 return nil, nil, "", false
18 }
19 r.Body = http.MaxBytesReader(rw, r.Body, 1<<20)
20 if !w.checkCSRF(r) {
21 http.Error(rw, "bad request", http.StatusForbidden)
22 return nil, nil, "", false
23 }
24 if w.Runs == nil {
25 w.notFound(rw, r)
26 return nil, nil, "", false
27 }
28 m, ok := w.open(rw, r)
29 if !ok {
30 return nil, nil, "", false
31 }
32 acl, err := m.ACL()
33 if err != nil || !acl.CanTriage(pr.account.ID) {
34 http.Error(rw, "only a maintainer or admin may manage CI", http.StatusForbidden)
35 return nil, nil, "", false
36 }
37 run, err := w.Runs.Get(r.PathValue("id"))
38 if err != nil || run == nil || run.Repo != m.Name {
39 w.notFound(rw, r)
40 return nil, nil, "", false
41 }
42 return m, run, pr.account.ID, true
43 }
44
45 func (w *Web) CancelRun(rw http.ResponseWriter, r *http.Request) {
46 m, run, accountID, ok := w.runAction(rw, r)
47 if !ok {
48 return
49 }
50 canceled, err := w.Runs.CancelQueued(run.ID, m.Name, time.Now().UTC())
51 if err != nil {
52 w.serverError(rw, r, "cannot cancel the run: "+err.Error())
53 return
54 }
55 if !canceled {
56 http.Error(rw, "only a queued run can be canceled", http.StatusBadRequest)
57 return
58 }
59 w.audit("web.run.cancel", accountID, m.Name+" "+run.ID)
60 http.Redirect(rw, r, "/r/"+m.Name+"/runs/"+run.ID, http.StatusSeeOther)
61 }
62
63 func (w *Web) Rerun(rw http.ResponseWriter, r *http.Request) {
64 m, run, accountID, ok := w.runAction(rw, r)
65 if !ok {
66 return
67 }
68 if run.Status == runs.StatusQueued || run.Status == runs.StatusRunning {
69 http.Error(rw, "this run has not finished yet", http.StatusBadRequest)
70 return
71 }
72 queued, err := w.Runs.Requeue(run.ID, m.Name, time.Now().UTC())
73 if err != nil {
74 w.serverError(rw, r, "cannot queue the run: "+err.Error())
75 return
76 }
77 if !queued {
78 http.Error(rw, "only a finished run can be rerun", http.StatusBadRequest)
79 return
80 }
81 w.audit("web.run.rerun", accountID, m.Name+" "+run.ID)
82 http.Redirect(rw, r, "/r/"+m.Name+"/runs/"+run.ID, http.StatusSeeOther)
83 }