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 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/workflows.go +84

@@ -0,0 +1,84 @@
1 package web
2
3 // Workflows are read from the default branch's tree, with the same validator the
4 // push boundary uses. The page is a view of what is configured, not a second
5 // scheduler: a run still happens when a matching push arrives.
6
7 import (
8 "net/http"
9
10 "github.com/rodent-software/vole/ci"
11 "github.com/rodent-software/vole/ci/runs"
12 )
13
14 type workflowJobView struct {
15 ID string
16 Labels []string
17 Image string
18 Steps int
19 Artifacts int
20 Pages bool
21 Last *runs.Run
22 }
23
24 type workflowView struct {
25 Path string
26 Branches []string
27 Tags []string
28 Jobs []workflowJobView
29 }
30
31 func (w *Web) WorkflowsPage(rw http.ResponseWriter, r *http.Request) {
32 m, ok := w.open(rw, r)
33 if !ok {
34 return
35 }
36 commit, _ := m.Git().RevParse(m.DefaultBranch)
37 workflows, problems := ci.At(m.Git(), commit)
38
39 var recent []*runs.Run
40 if w.Runs != nil {
41 recent, _ = w.Runs.Recent(m.Name, 100)
42 }
43 latest := map[string]*runs.Run{}
44 for _, run := range recent {
45 key := run.Workflow + "\x00" + run.Job
46 if latest[key] == nil {
47 latest[key] = run
48 }
49 }
50
51 views := make([]workflowView, 0, len(workflows))
52 for _, wf := range workflows {
53 view := workflowView{Path: wf.Path, Branches: wf.Branches, Tags: wf.Tags}
54 for _, job := range wf.Jobs {
55 image := job.Image
56 if job.ImageFile != "" {
57 image = "file:" + job.ImageFile
58 }
59 view.Jobs = append(view.Jobs, workflowJobView{
60 ID: job.ID, Labels: job.Labels, Image: image,
61 Steps: len(job.Steps), Artifacts: len(job.Artifacts),
62 Pages: job.Pages != nil,
63 Last: latest[wf.Path+"\x00"+job.ID],
64 })
65 }
66 views = append(views, view)
67 }
68
69 w.render(rw, "workflows", struct {
70 Page
71 Name string
72 Ref string
73 Workflows []workflowView
74 Problems []string
75 HasCommit bool
76 }{
77 Page: w.page(r, m.Name+" · workflows"),
78 Name: m.Name,
79 Ref: m.DefaultBranch,
80 Workflows: views,
81 Problems: problems,
82 HasCommit: commit != "",
83 }, 0)
84 }