internal/control/build.go
214 lines · 6666 bytes
1package control
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "strconv"
8
9 "gitbay.org/gitbay/internal/policy"
10 "gitbay.org/gitbay/internal/protocol"
11 "gitbay.org/gitbay/internal/store"
12)
13
14func init() {
15 register(Command{Path: []string{"build", "list"},
16 Summary: "list recent builds: build list <owner/name>", ReadOnly: true, Run: runBuildList})
17 register(Command{Path: []string{"build", "show"},
18 Summary: "show one build: build show <owner/name> <n>", ReadOnly: true, Run: runBuildShow})
19 register(Command{Path: []string{"build", "log"},
20 Summary: "print a build's log: build log <owner/name> <n>", ReadOnly: true, Run: runBuildLog})
21
22 // Runner commands: the claim/report loop for gitbay-runner. Admin-only —
23 // a runner executes arbitrary repo code, so handing out jobs is the
24 // instance operator's call.
25 register(Command{Path: []string{"runner", "next"},
26 Summary: "claim the oldest pending build (runner protocol)", SSHOnly: true, Run: runRunnerNext})
27 register(Command{Path: []string{"runner", "log"},
28 Summary: "append a build's log from stdin: runner log <build-id>", SSHOnly: true, ReadsStdin: true, Run: runRunnerLog})
29 register(Command{Path: []string{"runner", "done"},
30 Summary: "finish a build: runner done <build-id> success|failure", SSHOnly: true, Run: runRunnerDone})
31}
32
33type buildOut struct {
34 Number int64 `json:"number"`
35 Job string `json:"job"`
36 Status string `json:"status"`
37 SHA string `json:"sha"`
38 Ref string `json:"ref"`
39 CreatedAt string `json:"created_at"`
40 FinishedAt string `json:"finished_at,omitempty"`
41}
42
43func buildToOut(b store.Build) buildOut {
44 return buildOut{b.Number, b.Job, b.Status, b.SHA, b.Ref, b.CreatedAt, b.FinishedAt}
45}
46
47func buildRef(c *Ctx, args []string) (store.Repo, store.Build, int) {
48 if len(args) != 2 {
49 return store.Repo{}, store.Build{}, c.fail(protocol.ExitUsage, "expected <owner/name> <number>")
50 }
51 repo, code := resolveRepo(c, args[0], policy.CanRead)
52 if code >= 0 {
53 return repo, store.Build{}, code
54 }
55 n, err := strconv.ParseInt(args[1], 10, 64)
56 if err != nil {
57 return repo, store.Build{}, c.fail(protocol.ExitUsage, "bad build number %q", args[1])
58 }
59 b, err := c.Store.BuildByNumber(repo.ID, n)
60 if err != nil {
61 return repo, b, c.fail(protocol.ExitNotFound, "no build %d on %s", n, repo.Path())
62 }
63 return repo, b, -1
64}
65
66func runBuildList(c *Ctx, args []string) int {
67 if len(args) != 1 {
68 return c.fail(protocol.ExitUsage, "usage: build list <owner/name>")
69 }
70 repo, code := resolveRepo(c, args[0], policy.CanRead)
71 if code >= 0 {
72 return code
73 }
74 builds, err := c.Store.ListBuilds(repo.ID, 50)
75 if err != nil {
76 return c.fail(protocol.ExitFailure, "%v", err)
77 }
78 var ds []buildOut
79 for _, b := range builds {
80 ds = append(ds, buildToOut(b))
81 }
82 return c.emit(ds, func(w io.Writer) {
83 for _, d := range ds {
84 fmt.Fprintf(w, "%d\t%s\t%s\t%.10s\t%s\n", d.Number, d.Job, d.Status, d.SHA, d.Ref)
85 }
86 })
87}
88
89func runBuildShow(c *Ctx, args []string) int {
90 _, b, code := buildRef(c, args)
91 if code >= 0 {
92 return code
93 }
94 d := buildToOut(b)
95 return c.emit(d, func(w io.Writer) {
96 fmt.Fprintf(w, "build %d\t%s\t%s\n%.10s on %s\nqueued %s", d.Number, d.Job, d.Status, d.SHA, d.Ref, d.CreatedAt)
97 if d.FinishedAt != "" {
98 fmt.Fprintf(w, ", finished %s", d.FinishedAt)
99 }
100 fmt.Fprintln(w)
101 })
102}
103
104func runBuildLog(c *Ctx, args []string) int {
105 _, b, code := buildRef(c, args)
106 if code >= 0 {
107 return code
108 }
109 log, err := c.Store.BuildLog(b.ID)
110 if err != nil {
111 return c.fail(protocol.ExitFailure, "%v", err)
112 }
113 c.Stdout.Write(log)
114 return protocol.ExitOK
115}
116
117func requireRunner(c *Ctx) int {
118 if !c.User.IsAdmin {
119 return c.fail(protocol.ExitDenied, "runner commands are for instance-admin runner accounts")
120 }
121 return -1
122}
123
124func runRunnerNext(c *Ctx, args []string) int {
125 if code := requireRunner(c); code >= 0 {
126 return code
127 }
128 b, ok, err := c.Store.ClaimBuild()
129 if err != nil {
130 return c.fail(protocol.ExitFailure, "%v", err)
131 }
132 if !ok {
133 return c.emit(map[string]any{}, func(w io.Writer) { fmt.Fprintln(w, "no pending builds") })
134 }
135 repo, err := c.Store.RepoByID(b.RepoID)
136 if err != nil {
137 return c.fail(protocol.ExitFailure, "%v", err)
138 }
139 var steps []string
140 json.Unmarshal([]byte(b.Steps), &steps)
141 d := struct {
142 ID int64 `json:"id"`
143 Repo string `json:"repo"`
144 Number int64 `json:"number"`
145 Job string `json:"job"`
146 SHA string `json:"sha"`
147 Ref string `json:"ref"`
148 Steps []string `json:"steps"`
149 }{b.ID, repo.Path(), b.Number, b.Job, b.SHA, b.Ref, steps}
150 return c.emit(d, func(w io.Writer) {
151 fmt.Fprintf(w, "build %d: %s %s @ %.10s\n", d.ID, d.Repo, d.Job, d.SHA)
152 })
153}
154
155func runRunnerLog(c *Ctx, args []string) int {
156 if code := requireRunner(c); code >= 0 {
157 return code
158 }
159 if len(args) != 1 {
160 return c.fail(protocol.ExitUsage, "usage: runner log <build-id> (chunk on stdin)")
161 }
162 id, err := strconv.ParseInt(args[0], 10, 64)
163 if err != nil {
164 return c.fail(protocol.ExitUsage, "bad build id %q", args[0])
165 }
166 // Stream stdin into the log in chunks so long builds appear live.
167 buf := make([]byte, 64<<10)
168 for {
169 n, rerr := c.Stdin.Read(buf)
170 if n > 0 {
171 if err := c.Store.AppendBuildLog(id, buf[:n]); err != nil {
172 return c.fail(protocol.ExitFailure, "%v", err)
173 }
174 }
175 if rerr != nil {
176 break
177 }
178 }
179 return c.emit(map[string]string{"log": "ok"}, func(w io.Writer) {})
180}
181
182func runRunnerDone(c *Ctx, args []string) int {
183 if code := requireRunner(c); code >= 0 {
184 return code
185 }
186 if len(args) != 2 || (args[1] != "success" && args[1] != "failure") {
187 return c.fail(protocol.ExitUsage, "usage: runner done <build-id> success|failure")
188 }
189 id, err := strconv.ParseInt(args[0], 10, 64)
190 if err != nil {
191 return c.fail(protocol.ExitUsage, "bad build id %q", args[0])
192 }
193 b, err := c.Store.BuildByID(id)
194 if err != nil {
195 return c.fail(protocol.ExitNotFound, "no build %d", id)
196 }
197 if err := c.Store.FinishBuild(id, args[1]); err != nil {
198 return c.fail(protocol.ExitFailure, "finishing build %d: %v", id, err)
199 }
200 repo, err := c.Store.RepoByID(b.RepoID)
201 if err != nil {
202 return c.fail(protocol.ExitFailure, "%v", err)
203 }
204 url := fmt.Sprintf("%s/%s/builds/%d", c.Cfg.Server.SiteURL, repo.Path(), b.Number)
205 desc := "build " + args[1]
206 if err := c.Store.SetCommitStatus(repo.ID, b.SHA, "ci/"+b.Job, args[1], desc, url, c.User.ID); err != nil {
207 return c.fail(protocol.ExitFailure, "%v", err)
208 }
209 c.Store.RecordEvent(repo.ID, c.User.ID, "build."+args[1],
210 fmt.Sprintf(`{"number":%d,"job":%q}`, b.Number, b.Job))
211 return c.emit(map[string]any{"build": b.Number, "status": args[1]}, func(w io.Writer) {
212 fmt.Fprintf(w, "build %d %s\n", b.Number, args[1])
213 })
214}