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