internal/control/build_test.go
718 lines · 27828 bytes
18 symbols in this file
testZeroSHAgitTestEnvgitRunnernewQueueTestRepoTestQueueBranchBuildsFailsOpenOnDiffFailureTestQueueBranchBuildsNewBranchIgnoredPathSkipsTestQueueBranchBuildsNewBranchMatchedPathQueuesTestQueueBranchBuildsNewBranchFailsOpenWithoutMergeBaseTestQueueBranchBuildsFreshDefaultBranchFailsOpenTestQueueBranchBuildsOrdinaryPushStillFiltersTestQueueBranchBuildsFilteredJobRecordsSkippedStatusTestQueueBranchBuildsTagAndScheduledJobsRecordNoSkippedStatusTestQueueBranchBuildsAlreadyBuiltJobRecordsNoSkippedStatusTestQueueMRBuildsStillFailsOpenTestQueueBranchBuildsRebaseFiltersAgainstMergeBaseTestQueueBranchBuildsSameTreeReusesSuccessTestBuildListFlagsFilterTestQueueBranchBuildsRebuildsWhatOnlyAForkBuilt
1package control
2
3import (
4 "bytes"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "gitbay.org/gitbay/internal/protocol"
13 "gitbay.org/gitbay/internal/store"
14)
15
16const testZeroSHA = "0000000000000000000000000000000000000000"
17
18// gitTestEnv sets up an isolated git identity so tests never touch a
19// developer's real config.
20func gitTestEnv() []string {
21 return append(os.Environ(),
22 "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null",
23 "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test",
24 "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test")
25}
26
27func gitRunner(t *testing.T) func(dir string, args ...string) string {
28 t.Helper()
29 env := gitTestEnv()
30 return func(dir string, args ...string) string {
31 t.Helper()
32 cmd := exec.Command("git", args...)
33 cmd.Dir = dir
34 cmd.Env = env
35 out, err := cmd.CombinedOutput()
36 if err != nil {
37 t.Fatalf("git %v: %v\n%s", args, err, out)
38 }
39 return string(out)
40 }
41}
42
43// newQueueTestRepo returns a store with one public repo (default branch
44// "main", matching the schema default) and the uid to queue builds as.
45//
46// A real file, not ":memory:": ":memory:" gives each connection its own
47// database, so a goroutine querying while another writes (build log
48// --follow) sees an empty schema (see internal/store/contention_test.go).
49func newQueueTestRepo(t *testing.T) (*store.Store, store.Repo, int64) {
50 t.Helper()
51 st, err := store.Open(filepath.Join(t.TempDir(), "gitbay.db"))
52 if err != nil {
53 t.Fatal(err)
54 }
55 t.Cleanup(func() { st.Close() })
56 if err := st.MigrateUp(); err != nil {
57 t.Fatal(err)
58 }
59 uid, err := st.CreateUser("alice", false)
60 if err != nil {
61 t.Fatal(err)
62 }
63 repoID, err := st.CreateRepo("user", uid, "app", "public")
64 if err != nil {
65 t.Fatal(err)
66 }
67 repo, err := st.RepoByID(repoID)
68 if err != nil {
69 t.Fatal(err)
70 }
71 return st, repo, uid
72}
73
74// A DiffFiles failure must not turn into a silent skip: when the old sha
75// on record cannot be diffed against, every job runs regardless of what
76// it names in paths, the same as when there is no diff base at all.
77func TestQueueBranchBuildsFailsOpenOnDiffFailure(t *testing.T) {
78 st, repo, uid := newQueueTestRepo(t)
79 git := gitRunner(t)
80
81 root := t.TempDir()
82
83 // A job whose paths would exclude a docs-only change, so the test
84 // proves something: without fail-open, the diff failure would leave
85 // the filter unevaluated and this build would never queue.
86 src := filepath.Join(root, "src")
87 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
88 os.MkdirAll(filepath.Join(src, "docs"), 0o755)
89 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
90 "jobs:\n unit:\n paths:\n - src/**\n steps:\n - echo hi\n"), 0o644)
91 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644)
92 git(root, "init", "-q", "-b", "main", "src")
93 git(src, "add", ".")
94 git(src, "commit", "-q", "-m", "base")
95 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x changed\n"), 0o644)
96 git(src, "add", ".")
97 git(src, "commit", "-q", "-m", "docs only")
98 newSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
99
100 dir := RepoDir(root, repo.OwnerName, repo.Name)
101 os.MkdirAll(filepath.Dir(dir), 0o755)
102 git(root, "clone", "-q", "--bare", src, dir)
103
104 // Well-formed but names no object in this repo: git diff itself
105 // fails, rather than the empty/all-zero short-circuit HasDiffBase
106 // already covers.
107 old := strings.Repeat("1", 40)
108
109 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", old, newSHA, time.Now())
110
111 builds, err := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
112 if err != nil || len(builds) != 1 {
113 t.Fatalf("builds after queue: %v %v", builds, err)
114 }
115 if builds[0].Job != "unit" || builds[0].Status != "pending" || builds[0].SHA != newSHA {
116 t.Fatalf("queued build wrong: %+v", builds[0])
117 }
118}
119
120// A new branch's first push carries no old sha, but a diff base still
121// exists: the merge base with the default branch. A push whose commits
122// only touch paths a job ignores must not queue that job, or path
123// filters never do anything on the ordinary branch-then-MR workflow.
124func TestQueueBranchBuildsNewBranchIgnoredPathSkips(t *testing.T) {
125 st, repo, uid := newQueueTestRepo(t)
126 git := gitRunner(t)
127 root := t.TempDir()
128
129 src := filepath.Join(root, "src")
130 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
131 os.MkdirAll(filepath.Join(src, "docs"), 0o755)
132 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
133 "jobs:\n unit:\n paths-ignore:\n - docs/**\n steps:\n - echo hi\n"), 0o644)
134 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644)
135 git(root, "init", "-q", "-b", "main", "src")
136 git(src, "add", ".")
137 git(src, "commit", "-q", "-m", "base")
138
139 git(src, "checkout", "-q", "-b", "feature")
140 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x changed\n"), 0o644)
141 git(src, "add", ".")
142 git(src, "commit", "-q", "-m", "docs only")
143 featureSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
144
145 dir := RepoDir(root, repo.OwnerName, repo.Name)
146 os.MkdirAll(filepath.Dir(dir), 0o755)
147 git(root, "clone", "-q", "--bare", src, dir)
148
149 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "feature", testZeroSHA, featureSHA, time.Now())
150
151 builds, err := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
152 if err != nil {
153 t.Fatal(err)
154 }
155 if len(builds) != 0 {
156 t.Fatalf("docs-only push on a new branch queued a build: %+v", builds)
157 }
158}
159
160// The same new-branch push, but touching a path the job cares about:
161// the merge-base diff must still let it through.
162func TestQueueBranchBuildsNewBranchMatchedPathQueues(t *testing.T) {
163 st, repo, uid := newQueueTestRepo(t)
164 git := gitRunner(t)
165 root := t.TempDir()
166
167 src := filepath.Join(root, "src")
168 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
169 os.MkdirAll(filepath.Join(src, "src"), 0o755)
170 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
171 "jobs:\n unit:\n paths:\n - src/**\n steps:\n - echo hi\n"), 0o644)
172 os.WriteFile(filepath.Join(src, "src", "x.go"), []byte("package x\n"), 0o644)
173 git(root, "init", "-q", "-b", "main", "src")
174 git(src, "add", ".")
175 git(src, "commit", "-q", "-m", "base")
176
177 git(src, "checkout", "-q", "-b", "feature")
178 os.WriteFile(filepath.Join(src, "src", "x.go"), []byte("package x\n\nvar y int\n"), 0o644)
179 git(src, "add", ".")
180 git(src, "commit", "-q", "-m", "src change")
181 featureSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
182
183 dir := RepoDir(root, repo.OwnerName, repo.Name)
184 os.MkdirAll(filepath.Dir(dir), 0o755)
185 git(root, "clone", "-q", "--bare", src, dir)
186
187 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "feature", testZeroSHA, featureSHA, time.Now())
188
189 builds, err := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
190 if err != nil || len(builds) != 1 {
191 t.Fatalf("builds after queue: %v %v", builds, err)
192 }
193 if builds[0].Job != "unit" || builds[0].SHA != featureSHA {
194 t.Fatalf("queued build wrong: %+v", builds[0])
195 }
196}
197
198// When the new branch shares no history with the default branch, the
199// merge base cannot be computed. That must fail open, same as any other
200// diff base that cannot be evaluated.
201func TestQueueBranchBuildsNewBranchFailsOpenWithoutMergeBase(t *testing.T) {
202 st, repo, uid := newQueueTestRepo(t)
203 git := gitRunner(t)
204 root := t.TempDir()
205
206 dir := RepoDir(root, repo.OwnerName, repo.Name)
207 os.MkdirAll(filepath.Dir(dir), 0o755)
208 git(root, "init", "-q", "--bare", dir)
209
210 mainSrc := filepath.Join(root, "main-src")
211 os.MkdirAll(filepath.Join(mainSrc, ".gitbay"), 0o755)
212 os.WriteFile(filepath.Join(mainSrc, ".gitbay", "ci.yml"), []byte(
213 "jobs:\n unit:\n paths-ignore:\n - docs/**\n steps:\n - echo hi\n"), 0o644)
214 git(root, "init", "-q", "-b", "main", "main-src")
215 git(mainSrc, "add", ".")
216 git(mainSrc, "commit", "-q", "-m", "base")
217 git(mainSrc, "push", "-q", dir, "main")
218
219 // An unrelated repository: no common commit with main.
220 otherSrc := filepath.Join(root, "other-src")
221 os.MkdirAll(filepath.Join(otherSrc, ".gitbay"), 0o755)
222 os.MkdirAll(filepath.Join(otherSrc, "docs"), 0o755)
223 os.WriteFile(filepath.Join(otherSrc, ".gitbay", "ci.yml"), []byte(
224 "jobs:\n unit:\n paths-ignore:\n - docs/**\n steps:\n - echo hi\n"), 0o644)
225 os.WriteFile(filepath.Join(otherSrc, "docs", "x.md"), []byte("# x\n"), 0o644)
226 git(root, "init", "-q", "-b", "feature", "other-src")
227 git(otherSrc, "add", ".")
228 git(otherSrc, "commit", "-q", "-m", "unrelated docs-only")
229 otherSHA := strings.TrimSpace(git(otherSrc, "rev-parse", "HEAD"))
230 git(otherSrc, "push", "-q", dir, "feature")
231
232 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "feature", testZeroSHA, otherSHA, time.Now())
233
234 builds, err := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
235 if err != nil || len(builds) != 1 {
236 t.Fatalf("expected fail-open to queue the job: %v %v", builds, err)
237 }
238}
239
240// The first push to a brand-new repository moves the default branch
241// itself with no prior commit: the merge base of the default branch
242// against its own tip is the tip, which carries no diff. That must
243// fail open rather than read as "nothing changed".
244func TestQueueBranchBuildsFreshDefaultBranchFailsOpen(t *testing.T) {
245 st, repo, uid := newQueueTestRepo(t)
246 git := gitRunner(t)
247 root := t.TempDir()
248
249 src := filepath.Join(root, "src")
250 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
251 os.MkdirAll(filepath.Join(src, "docs"), 0o755)
252 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
253 "jobs:\n unit:\n paths:\n - src/**\n steps:\n - echo hi\n"), 0o644)
254 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644)
255 git(root, "init", "-q", "-b", "main", "src")
256 git(src, "add", ".")
257 git(src, "commit", "-q", "-m", "initial")
258 sha := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
259
260 dir := RepoDir(root, repo.OwnerName, repo.Name)
261 os.MkdirAll(filepath.Dir(dir), 0o755)
262 git(root, "clone", "-q", "--bare", src, dir)
263
264 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", testZeroSHA, sha, time.Now())
265
266 builds, err := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
267 if err != nil || len(builds) != 1 {
268 t.Fatalf("expected fail-open on the repository's first commit: %v %v", builds, err)
269 }
270}
271
272// An ordinary push with a genuine old sha is unaffected by the
273// new-branch handling: filtering still works exactly as it did before.
274func TestQueueBranchBuildsOrdinaryPushStillFilters(t *testing.T) {
275 st, repo, uid := newQueueTestRepo(t)
276 git := gitRunner(t)
277 root := t.TempDir()
278
279 src := filepath.Join(root, "src")
280 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
281 os.MkdirAll(filepath.Join(src, "docs"), 0o755)
282 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
283 "jobs:\n unit:\n paths-ignore:\n - docs/**\n steps:\n - echo hi\n"), 0o644)
284 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644)
285 git(root, "init", "-q", "-b", "main", "src")
286 git(src, "add", ".")
287 git(src, "commit", "-q", "-m", "base")
288 oldSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
289
290 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x changed\n"), 0o644)
291 git(src, "add", ".")
292 git(src, "commit", "-q", "-m", "docs only")
293 newSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
294
295 dir := RepoDir(root, repo.OwnerName, repo.Name)
296 os.MkdirAll(filepath.Dir(dir), 0o755)
297 git(root, "clone", "-q", "--bare", src, dir)
298
299 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", oldSHA, newSHA, time.Now())
300
301 builds, err := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
302 if err != nil {
303 t.Fatal(err)
304 }
305 if len(builds) != 0 {
306 t.Fatalf("docs-only push with a real old sha queued a build: %+v", builds)
307 }
308}
309
310// A job a path filter excludes records a ci/<job> status of "skipped"
311// naming the reason, rather than leaving the commit with no status for
312// that job at all (#172).
313func TestQueueBranchBuildsFilteredJobRecordsSkippedStatus(t *testing.T) {
314 st, repo, uid := newQueueTestRepo(t)
315 git := gitRunner(t)
316 root := t.TempDir()
317
318 src := filepath.Join(root, "src")
319 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
320 os.MkdirAll(filepath.Join(src, "docs"), 0o755)
321 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
322 "jobs:\n unit:\n paths-ignore:\n - docs/**\n steps:\n - echo hi\n"), 0o644)
323 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644)
324 git(root, "init", "-q", "-b", "main", "src")
325 git(src, "add", ".")
326 git(src, "commit", "-q", "-m", "base")
327 oldSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
328
329 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x changed\n"), 0o644)
330 git(src, "add", ".")
331 git(src, "commit", "-q", "-m", "docs only")
332 newSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
333
334 dir := RepoDir(root, repo.OwnerName, repo.Name)
335 os.MkdirAll(filepath.Dir(dir), 0o755)
336 git(root, "clone", "-q", "--bare", src, dir)
337
338 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", oldSHA, newSHA, time.Now())
339
340 statuses, err := st.ListCommitStatuses(repo.ID, newSHA)
341 if err != nil {
342 t.Fatal(err)
343 }
344 if len(statuses) != 1 || statuses[0].Context != "ci/unit" {
345 t.Fatalf("statuses on %s: %+v", newSHA, statuses)
346 }
347 if statuses[0].State != "skipped" {
348 t.Fatalf("filtered job status: got %q, want skipped", statuses[0].State)
349 }
350 if statuses[0].Description == "" {
351 t.Fatal("skipped status carries no reason")
352 }
353}
354
355// A tag job and a scheduled job are not push jobs at all: neither records
356// a skipped status, filtered push or not. Only the ordinary job that a
357// path filter actually excluded does.
358func TestQueueBranchBuildsTagAndScheduledJobsRecordNoSkippedStatus(t *testing.T) {
359 st, repo, uid := newQueueTestRepo(t)
360 git := gitRunner(t)
361 root := t.TempDir()
362
363 src := filepath.Join(root, "src")
364 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
365 os.MkdirAll(filepath.Join(src, "docs"), 0o755)
366 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(strings.Join([]string{
367 "jobs:",
368 " unit:",
369 " paths-ignore:",
370 " - docs/**",
371 " steps:",
372 " - echo hi",
373 " release:",
374 " tags: 'v*'",
375 " steps:",
376 " - echo release",
377 " nightly:",
378 " schedule: '0 0 * * *'",
379 " steps:",
380 " - echo nightly",
381 "",
382 }, "\n")), 0o644)
383 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644)
384 git(root, "init", "-q", "-b", "main", "src")
385 git(src, "add", ".")
386 git(src, "commit", "-q", "-m", "base")
387 oldSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
388
389 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x changed\n"), 0o644)
390 git(src, "add", ".")
391 git(src, "commit", "-q", "-m", "docs only")
392 newSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
393
394 dir := RepoDir(root, repo.OwnerName, repo.Name)
395 os.MkdirAll(filepath.Dir(dir), 0o755)
396 git(root, "clone", "-q", "--bare", src, dir)
397
398 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", oldSHA, newSHA, time.Now())
399
400 statuses, err := st.ListCommitStatuses(repo.ID, newSHA)
401 if err != nil {
402 t.Fatal(err)
403 }
404 if len(statuses) != 1 || statuses[0].Context != "ci/unit" || statuses[0].State != "skipped" {
405 t.Fatalf("statuses on %s: %+v", newSHA, statuses)
406 }
407}
408
409// A job already built for this commit on another branch is a fact about
410// the commit, not something a filter excluded: it keeps whatever status
411// that build reported (or none, if the run is still queued elsewhere) and
412// must not be overwritten with "skipped".
413func TestQueueBranchBuildsAlreadyBuiltJobRecordsNoSkippedStatus(t *testing.T) {
414 st, repo, uid := newQueueTestRepo(t)
415 git := gitRunner(t)
416 root := t.TempDir()
417
418 src := filepath.Join(root, "src")
419 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
420 os.MkdirAll(filepath.Join(src, "docs"), 0o755)
421 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
422 "jobs:\n unit:\n paths-ignore:\n - docs/**\n steps:\n - echo hi\n"), 0o644)
423 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644)
424 git(root, "init", "-q", "-b", "main", "src")
425 git(src, "add", ".")
426 git(src, "commit", "-q", "-m", "base")
427 oldSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
428
429 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x changed\n"), 0o644)
430 git(src, "add", ".")
431 git(src, "commit", "-q", "-m", "docs only")
432 newSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
433
434 dir := RepoDir(root, repo.OwnerName, repo.Name)
435 os.MkdirAll(filepath.Dir(dir), 0o755)
436 git(root, "clone", "-q", "--bare", src, dir)
437
438 // The same commit already has a build for "unit" from another branch,
439 // still pending. Its filter would exclude this push too, so the only
440 // way to tell the two paths apart is that this one must record nothing.
441 if _, err := st.CreateBuild(repo.ID, "unit", newSHA, "other", `["echo hi"]`, "", "", true); err != nil {
442 t.Fatal(err)
443 }
444
445 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", oldSHA, newSHA, time.Now())
446
447 statuses, err := st.ListCommitStatuses(repo.ID, newSHA)
448 if err != nil {
449 t.Fatal(err)
450 }
451 if len(statuses) != 0 {
452 t.Fatalf("already-built job recorded a status: %+v", statuses)
453 }
454 builds, err := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
455 if err != nil || len(builds) != 1 {
456 t.Fatalf("expected only the pre-existing build: %v %v", builds, err)
457 }
458}
459
460// QueueMRBuilds keeps failing open with no diff base at all: deriving a
461// merge base for the MR head is deliberately out of scope here (#172 —
462// filtering a head down to zero jobs leaves it with no statuses, which
463// the require_checks gate reads as unmergeable). This test documents
464// and locks in that choice.
465func TestQueueMRBuildsStillFailsOpen(t *testing.T) {
466 st, repo, uid := newQueueTestRepo(t)
467 git := gitRunner(t)
468 root := t.TempDir()
469
470 src := filepath.Join(root, "src")
471 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
472 os.MkdirAll(filepath.Join(src, "docs"), 0o755)
473 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
474 "jobs:\n unit:\n paths-ignore:\n - docs/**\n steps:\n - echo hi\n"), 0o644)
475 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644)
476 git(root, "init", "-q", "-b", "main", "src")
477 git(src, "add", ".")
478 git(src, "commit", "-q", "-m", "base")
479
480 git(src, "checkout", "-q", "-b", "pr")
481 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x changed\n"), 0o644)
482 git(src, "add", ".")
483 git(src, "commit", "-q", "-m", "docs only")
484 prSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
485
486 dir := RepoDir(root, repo.OwnerName, repo.Name)
487 os.MkdirAll(filepath.Dir(dir), 0o755)
488 git(root, "clone", "-q", "--bare", src, dir)
489 git(dir, "update-ref", "refs/merge-requests/1/head", prSHA)
490
491 QueueMRBuilds(st, root, "https://x.test", repo, uid, 1, prSHA)
492
493 builds, err := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
494 if err != nil || len(builds) != 1 {
495 t.Fatalf("expected the MR head to fail open and queue a build: %v %v", builds, err)
496 }
497}
498
499// A force-push that rewrote the branch must not filter against the old
500// tip. After a rebase, old..new is the difference between two histories
501// — whatever the new base added — so a branch whose own commits touch
502// code looks like a docs-only push and its jobs are skipped. It then
503// reads as green without having run (#176).
504func TestQueueBranchBuildsRebaseFiltersAgainstMergeBase(t *testing.T) {
505 st, repo, uid := newQueueTestRepo(t)
506 git := gitRunner(t)
507 root := t.TempDir()
508
509 src := filepath.Join(root, "src")
510 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
511 os.MkdirAll(filepath.Join(src, "docs"), 0o755)
512 os.MkdirAll(filepath.Join(src, "app"), 0o755)
513 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
514 "jobs:\n unit:\n paths-ignore:\n - docs/**\n steps:\n - echo hi\n"), 0o644)
515 os.WriteFile(filepath.Join(src, "app", "a.go"), []byte("package a\n"), 0o644)
516 git(root, "init", "-q", "-b", "main", "src")
517 git(src, "add", ".")
518 git(src, "commit", "-q", "-m", "base")
519
520 // A branch that changes code — the kind of change the filter must
521 // never skip.
522 git(src, "checkout", "-q", "-b", "feat")
523 os.WriteFile(filepath.Join(src, "app", "a.go"), []byte("package a\n\nvar X = 1\n"), 0o644)
524 git(src, "add", ".")
525 git(src, "commit", "-q", "-m", "code change")
526 oldTip := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
527
528 // main moves on with a docs-only commit, and the branch is rebased
529 // onto it — exactly what a fast-forward-only repository forces.
530 git(src, "checkout", "-q", "main")
531 os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644)
532 git(src, "add", ".")
533 git(src, "commit", "-q", "-m", "docs only")
534 git(src, "checkout", "-q", "feat")
535 git(src, "rebase", "-q", "main")
536 newTip := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
537
538 // The trap: between the two tips lies only the docs commit.
539 if diff := git(src, "diff", "--name-only", oldTip, newTip); !strings.Contains(diff, "docs/x.md") ||
540 strings.Contains(diff, "app/a.go") {
541 t.Fatalf("fixture does not reproduce the trap; old..new = %q", diff)
542 }
543
544 dir := RepoDir(root, repo.OwnerName, repo.Name)
545 os.MkdirAll(filepath.Dir(dir), 0o755)
546 git(root, "clone", "-q", "--bare", src, dir)
547
548 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "feat", oldTip, newTip, time.Now())
549
550 builds, err := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
551 if err != nil {
552 t.Fatal(err)
553 }
554 if len(builds) == 0 {
555 t.Fatal("a rebased branch whose commits change code queued no build")
556 }
557}
558
559// A rebase gives a commit a new sha and the same tree. A job that
560// succeeded for that tree has nothing left to prove, so the new commit
561// gets the earlier result as its status instead of a new build (#177).
562func TestQueueBranchBuildsSameTreeReusesSuccess(t *testing.T) {
563 st, repo, uid := newQueueTestRepo(t)
564 git := gitRunner(t)
565 root := t.TempDir()
566
567 src := filepath.Join(root, "src")
568 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
569 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
570 "jobs:\n unit:\n steps:\n - echo hi\n"), 0o644)
571 git(root, "init", "-q", "-b", "main", "src")
572 git(src, "add", ".")
573 git(src, "commit", "-q", "-m", "base")
574 first := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
575 // Same tree, new sha: what a rebase onto an unrelated base produces.
576 git(src, "commit", "-q", "--allow-empty", "-m", "rewritten")
577 second := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
578 if git(src, "rev-parse", first+"^{tree}") != git(src, "rev-parse", second+"^{tree}") {
579 t.Fatal("fixture: trees differ")
580 }
581
582 dir := RepoDir(root, repo.OwnerName, repo.Name)
583 os.MkdirAll(filepath.Dir(dir), 0o755)
584 git(root, "clone", "-q", "--bare", src, dir)
585
586 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", "", first, time.Now())
587 builds, _ := st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
588 if len(builds) != 1 {
589 t.Fatalf("first commit queued %d builds, want 1", len(builds))
590 }
591 if _, ok, err := st.ClaimBuild([]int64{repo.ID}, false); err != nil || !ok {
592 t.Fatalf("claim: ok=%v err=%v", ok, err)
593 }
594 if err := st.FinishBuild(builds[0].ID, "success"); err != nil {
595 t.Fatal(err)
596 }
597
598 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", first, second, time.Now())
599 builds, _ = st.ListBuilds(repo.ID, store.BuildFilter{}, 10)
600 if len(builds) != 1 {
601 t.Fatalf("same tree queued a second build: %+v", builds)
602 }
603 statuses, _ := st.ListCommitStatuses(repo.ID, second)
604 found := false
605 for _, s := range statuses {
606 if s.Context == "ci/unit" && s.State == "success" && strings.Contains(s.Description, "same tree") {
607 found = true
608 }
609 }
610 if !found {
611 t.Fatalf("second commit has no success status from the first: %+v", statuses)
612 }
613}
614
615// build list's --ref, --status and --job flags narrow the CLI listing the
616// same way the store filter does, and combine when more than one is given.
617// An invalid --status is refused rather than silently matching nothing (#224).
618func TestBuildListFlagsFilter(t *testing.T) {
619 st, repo, uid := newQueueTestRepo(t)
620 if _, err := st.CreateBuild(repo.ID, "unit", "aaa", "main", `["true"]`, "", "", true); err != nil {
621 t.Fatal(err)
622 }
623 if _, err := st.CreateBuild(repo.ID, "lint", "bbb", "feature", `["true"]`, "", "", true); err != nil {
624 t.Fatal(err)
625 }
626 if _, ok, err := st.ClaimBuild([]int64{repo.ID}, false); err != nil || !ok {
627 t.Fatalf("claim: %v ok=%v", err, ok)
628 }
629 if err := st.FinishBuild(1, "failure"); err != nil {
630 t.Fatal(err)
631 }
632 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid})
633
634 run := func(args ...string) string {
635 t.Helper()
636 out := c.Stdout.(*bytes.Buffer)
637 out.Reset()
638 errOut.Reset()
639 argv := append([]string{"build", "list", repo.Path()}, args...)
640 if code := Dispatch(c, argv); code != protocol.ExitOK {
641 t.Fatalf("build list %v: exit %d: %s", args, code, errOut.String())
642 }
643 return out.String()
644 }
645
646 if out := run(); !strings.Contains(out, "unit") || !strings.Contains(out, "lint") {
647 t.Fatalf("unfiltered listing missing a build:\n%s", out)
648 }
649 if out := run("--ref", "main"); !strings.Contains(out, "unit") || strings.Contains(out, "lint") {
650 t.Fatalf("--ref main:\n%s", out)
651 }
652 if out := run("--job", "lint"); strings.Contains(out, "unit") || !strings.Contains(out, "lint") {
653 t.Fatalf("--job lint:\n%s", out)
654 }
655 if out := run("--status", "failure"); !strings.Contains(out, "unit") || strings.Contains(out, "lint") {
656 t.Fatalf("--status failure:\n%s", out)
657 }
658 if out := run("--ref", "main", "--status", "pending"); strings.TrimSpace(out) != "" {
659 t.Fatalf("non-matching combination returned rows:\n%s", out)
660 }
661
662 out := c.Stdout.(*bytes.Buffer)
663 out.Reset()
664 errOut.Reset()
665 if code := Dispatch(c, []string{"build", "list", repo.Path(), "--status", "bogus"}); code != protocol.ExitUsage {
666 t.Fatalf("bad --status: exit %d, want %d (usage)", code, protocol.ExitUsage)
667 }
668 if !strings.Contains(errOut.String(), "pending") || !strings.Contains(errOut.String(), "cancelled") {
669 t.Fatalf("bad --status error does not name the valid states: %s", errOut.String())
670 }
671}
672
673// A fork's green build of a commit does not stand for the repository's
674// own: the same commit landing on a branch, or a commit with the same
675// tree, is built again as trusted (#258).
676func TestQueueBranchBuildsRebuildsWhatOnlyAForkBuilt(t *testing.T) {
677 st, repo, uid := newQueueTestRepo(t)
678 git := gitRunner(t)
679 root := t.TempDir()
680
681 src := filepath.Join(root, "src")
682 os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
683 os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(
684 "jobs:\n unit:\n steps:\n - echo hi\n"), 0o644)
685 git(root, "init", "-q", "-b", "main", "src")
686 git(src, "add", ".")
687 git(src, "commit", "-q", "-m", "base")
688 first := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
689 git(src, "commit", "-q", "--allow-empty", "-m", "same tree")
690 second := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
691
692 dir := RepoDir(root, repo.OwnerName, repo.Name)
693 os.MkdirAll(filepath.Dir(dir), 0o755)
694 git(root, "clone", "-q", "--bare", src, dir)
695
696 // A fork's merge request head, built untrusted and green.
697 QueueMRBuilds(st, root, "https://x.test", repo, uid, 1, first)
698 b, ok, err := st.ClaimBuild([]int64{repo.ID}, true)
699 if err != nil || !ok || b.Trusted {
700 t.Fatalf("claim: ok=%v trusted=%v err=%v", ok, b.Trusted, err)
701 }
702 if err := st.FinishBuild(b.ID, "success"); err != nil {
703 t.Fatal(err)
704 }
705
706 // The same commit lands on main, then a commit with the same tree.
707 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", "", first, time.Now())
708 QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", first, second, time.Now())
709 pending, _ := st.ListBuilds(repo.ID, store.BuildFilter{Status: "pending"}, 10)
710 if len(pending) != 2 {
711 t.Fatalf("queued %d builds, want 2 (one per commit): %+v", len(pending), pending)
712 }
713 for _, p := range pending {
714 if !p.Trusted {
715 t.Errorf("build %d queued untrusted on a branch push", p.Number)
716 }
717 }
718}