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.

Give the CLI rerun too

70291860685b74018f96a47686e5e6160e11f14f · Keenan Nemetz · 6d ago · parent deb024d4

Give the CLI rerun too

The run page offered Rerun while the CLI did not. It is the same maintainer
action against the same instance-side queue: `vole run rerun <id>` queues a
finished run again with the job it already pinned, refuses one that has not
finished, and writes the same `run.rerun` audit record the page writes.

Both transports now offer both run actions, which is what #168 asked of issues
and comments applied to CI.

server/ssh/run.go +72−35

@@ -1,18 +1,19 @@
1 1 package ssh
2 2
3 // Canceling a run over the SSH channel.
3 // Managing a run over the SSH channel.
4 4 //
5 5 // vole-run-cancel <repository> <run-id>
6 // vole-run-rerun <repository> <run-id>
6 7 //
7 // Canceling is a write to the queue, and the queue is instance-side, so the CLI
8 // asks the server the same way `vole change merge` does. This is the counterpart of
9 // the browser's cancel button: the same account that may cancel from the page may
10 // cancel here, the same two outcomes follow from the run's status, and the audit
11 // record names the account, the repository, and the run.
8 // Canceling and rerunning are writes to the queue, and the queue is instance-side,
9 // so the CLI asks the server the same way `vole change merge` does. They are the
10 // counterparts of the browser's buttons on a run page: the same account that may act
11 // from the page may act here, the same status decides what the command means, and
12 // the audit record names the account, the repository, and the run.
12 13 //
13 // The verb is not a shell. It takes one repository and one run id and answers one
14 // line; a queued run is retired, and a running one is asked to stop at its runner's
15 // next heartbeat, exactly as the page does.
14 // The verbs are not a shell. Each takes one repository and one run id and answers one
15 // line: a queued run is retired, a running one is asked to stop at its runner's next
16 // heartbeat, and a finished one can be queued again with the same pinned job.
16 17
17 18 import (
18 19 "fmt"
@@ -26,77 +27,89 @@ import (
26 27 "github.com/rodent-software/vole/internal/audit"
27 28 )
28 29
29 func (s *Server) serveRunCancel(ch xssh.Channel, argv []string, user string) int {
30 // openRunFor resolves the target of a run-management verb. It writes the refusal to
31 // ch and reports false when the caller should stop. Both verbs ask the same
32 // questions in the same order, so a name that is refused for one is refused for the
33 // other with the same words: the account, the repository, the maintainer capability,
34 // and then the run.
35 func (s *Server) openRunFor(ch xssh.Channel, argv []string, user, usage string) (*runs.Run, string, bool) {
30 36 if len(argv) != 3 {
31 fmt.Fprint(ch, "vole: usage: vole-run-cancel <repository> <run-id>\n")
32 return 129
37 fmt.Fprintf(ch, "vole: usage: %s\n", usage)
38 return nil, "", false
33 39 }
34 40 name, ok := repoName(argv[1])
35 41 if !ok {
36 42 fmt.Fprint(ch, "vole: invalid repository name\n")
37 return 129
43 return nil, "", false
38 44 }
39 45 runID := strings.TrimSpace(argv[2])
40 46 if !id.Valid(runID) {
41 47 fmt.Fprintf(ch, "vole: %q is not a run id; use the 26-character id from the run page\n", runID)
42 return 129
48 return nil, "", false
43 49 }
44 50 if s.cfg.Runs == nil {
45 51 fmt.Fprint(ch, "vole: this instance has no CI queue\n")
46 return 1
52 return nil, "", false
47 53 }
48 54 m, err := s.cfg.Repos.Open(name)
49 55 if err != nil {
50 56 fmt.Fprintf(ch, "vole: repository %q not found\n", name)
51 return 1
57 return nil, "", false
52 58 }
53 59 a, err := m.ACL()
54 60 if err != nil {
55 61 fmt.Fprintf(ch, "vole: cannot read access rules for %s\n", name)
56 return 1
62 return nil, "", false
57 63 }
58 64 // Managing a run is the maintainer capability, not triage of one item. This is
59 // the same check the browser's action makes, on the account this key resolved to.
65 // the same check the browser's actions make, on the account this key resolved to.
60 66 if !a.CanTriage(user) {
61 fmt.Fprintf(ch, "vole: %s may not cancel runs in %s; only a maintainer or admin may manage CI\n", user, name)
62 return 1
67 fmt.Fprintf(ch, "vole: %s may not manage runs in %s; only a maintainer or admin may manage CI\n", user, name)
68 return nil, "", false
63 69 }
64 70 run, err := s.cfg.Runs.Get(runID)
65 71 if err != nil {
66 72 fmt.Fprintf(ch, "vole: cannot read run %s: %v\n", runID, err)
67 return 1
73 return nil, "", false
68 74 }
69 75 if run == nil || run.Repo != name {
70 76 fmt.Fprintf(ch, "vole: no run %s in %s\n", runID, name)
71 return 1
77 return nil, "", false
72 78 }
79 return run, name, true
80 }
73 81
82 func (s *Server) serveRunCancel(ch xssh.Channel, argv []string, user string) int {
83 run, name, ok := s.openRunFor(ch, argv, user, "vole-run-cancel <repository> <run-id>")
84 if !ok {
85 return 129
86 }
74 87 now := time.Now().UTC()
75 88 switch run.Status {
76 89 case runs.StatusQueued:
77 ok, err := s.cfg.Runs.CancelQueued(run.ID, name, user, now)
90 canceled, err := s.cfg.Runs.CancelQueued(run.ID, name, user, now)
78 91 if err != nil {
79 92 fmt.Fprintf(ch, "vole: cannot cancel %s: %v\n", run.ID, err)
80 93 return 1
81 94 }
82 if !ok {
95 if !canceled {
83 96 fmt.Fprintf(ch, "vole: %s started before it could be canceled; try again\n", run.ID)
84 97 return 1
85 98 }
86 s.auditRunCancel(user, name, run.ID)
99 s.auditRun("run.cancel", user, name, run.ID)
87 100 fmt.Fprintf(ch, "canceled %s while it was waiting for a runner\n", run.ID)
88 101 return 0
89 102 case runs.StatusRunning:
90 ok, err := s.cfg.Runs.CancelRunning(run.ID, name, user, now)
103 canceled, err := s.cfg.Runs.CancelRunning(run.ID, name, user, now)
91 104 if err != nil {
92 105 fmt.Fprintf(ch, "vole: cannot stop %s: %v\n", run.ID, err)
93 106 return 1
94 107 }
95 if !ok {
108 if !canceled {
96 109 fmt.Fprintf(ch, "vole: %s finished before it could be stopped\n", run.ID)
97 110 return 1
98 111 }
99 s.auditRunCancel(user, name, run.ID)
112 s.auditRun("run.cancel", user, name, run.ID)
100 113 fmt.Fprintf(ch, "asked %s to stop; its runner stops at the next heartbeat\n", run.ID)
101 114 return 0
102 115 default:
@@ -106,14 +119,38 @@ func (s *Server) serveRunCancel(ch xssh.Channel, argv []string, user string) int
106 119 }
107 120 }
108 121
109 // auditRunCancel records the decision. It is the same action name the browser
110 // writes, because it is the same action: the transport is not what an operator
111 // looking for a canceled run wants to filter on. A failure is reported in the
112 // server log rather than refusing the cancel, which has already happened in the
113 // queue — the rule the browser's audit follows for the same reason.
114 func (s *Server) auditRunCancel(user, repo, runID string) {
122 func (s *Server) serveRunRerun(ch xssh.Channel, argv []string, user string) int {
123 run, name, ok := s.openRunFor(ch, argv, user, "vole-run-rerun <repository> <run-id>")
124 if !ok {
125 return 129
126 }
127 if run.Status == runs.StatusQueued || run.Status == runs.StatusRunning {
128 fmt.Fprintf(ch, "vole: %s is %s and has not finished yet; only a finished run can be rerun\n",
129 run.ID, run.Status)
130 return 1
131 }
132 queued, err := s.cfg.Runs.Requeue(run.ID, name, time.Now().UTC())
133 if err != nil {
134 fmt.Fprintf(ch, "vole: cannot queue %s again: %v\n", run.ID, err)
135 return 1
136 }
137 if !queued {
138 fmt.Fprintf(ch, "vole: %s changed before it could be rerun; try again\n", run.ID)
139 return 1
140 }
141 s.auditRun("run.rerun", user, name, run.ID)
142 fmt.Fprintf(ch, "queued %s again with the same pinned job\n", run.ID)
143 return 0
144 }
145
146 // auditRun records the decision. It is the same action name the browser writes,
147 // because it is the same action: the transport is not what an operator looking for
148 // canceled or rerun work wants to filter on. A failure is reported in the server log
149 // rather than refusing the action, which has already happened in the queue — the
150 // rule the browser's audit follows for the same reason.
151 func (s *Server) auditRun(action, user, repo, runID string) {
115 152 if err := audit.Append(s.cfg.AuditPath, audit.Record{
116 Action: "run.cancel",
153 Action: action,
117 154 Actor: user,
118 155 Subject: repo,
119 156 Detail: runID,