internal/control/build.go

0d12684707ba92a7a5dcf9aee6077df1f8c17c9c
gitbay/internal/control/build.go history · blame · raw

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}