| @@ -11,15 +11,42 @@ import ( |
| 11 | 11 | "gitbay.org/gitbay/internal/store" |
| 12 | 12 | ) |
| 13 | 13 | |
| 14 | | // A DiffFiles failure must not turn into a silent skip: when the old sha |
| 15 | | // on record cannot be diffed against, every job runs regardless of what |
| 16 | | // it names in paths, the same as when there is no diff base at all. |
| 17 | | func TestQueueBranchBuildsFailsOpenOnDiffFailure(t *testing.T) { |
| 14 | const testZeroSHA = "0000000000000000000000000000000000000000" |
| 15 | |
| 16 | // gitTestEnv sets up an isolated git identity so tests never touch a |
| 17 | // developer's real config. |
| 18 | func 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 | |
| 25 | func 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. |
| 43 | func newQueueTestRepo(t *testing.T) (*store.Store, store.Repo, int64) { |
| 44 | t.Helper() |
| 18 | 45 | st, err := store.Open(":memory:") |
| 19 | 46 | if err != nil { |
| 20 | 47 | t.Fatal(err) |
| 21 | 48 | } |
| 22 | | defer st.Close() |
| 49 | t.Cleanup(func() { st.Close() }) |
| 23 | 50 | if err := st.MigrateUp(); err != nil { |
| 24 | 51 | t.Fatal(err) |
| 25 | 52 | } |
| @@ -35,23 +62,17 @@ func TestQueueBranchBuildsFailsOpenOnDiffFailure(t *testing.T) { |
| 35 | 62 | if err != nil { |
| 36 | 63 | t.Fatal(err) |
| 37 | 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. |
| 71 | func TestQueueBranchBuildsFailsOpenOnDiffFailure(t *testing.T) { |
| 72 | st, repo, uid := newQueueTestRepo(t) |
| 73 | git := gitRunner(t) |
| 38 | 74 | |
| 39 | 75 | root := t.TempDir() |
| 40 | | env := append(os.Environ(), |
| 41 | | "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", |
| 42 | | "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test", |
| 43 | | "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test") |
| 44 | | git := func(dir string, args ...string) string { |
| 45 | | t.Helper() |
| 46 | | cmd := exec.Command("git", args...) |
| 47 | | cmd.Dir = dir |
| 48 | | cmd.Env = env |
| 49 | | out, err := cmd.CombinedOutput() |
| 50 | | if err != nil { |
| 51 | | t.Fatalf("git %v: %v\n%s", args, err, out) |
| 52 | | } |
| 53 | | return string(out) |
| 54 | | } |
| 55 | 76 | |
| 56 | 77 | // A job whose paths would exclude a docs-only change, so the test |
| 57 | 78 | // proves something: without fail-open, the diff failure would leave |
| @@ -89,3 +110,232 @@ func TestQueueBranchBuildsFailsOpenOnDiffFailure(t *testing.T) { |
| 89 | 110 | t.Fatalf("queued build wrong: %+v", builds[0]) |
| 90 | 111 | } |
| 91 | 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. |
| 118 | func 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. |
| 156 | func 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. |
| 195 | func 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". |
| 238 | func 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. |
| 268 | func 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 | // QueueMRBuilds keeps failing open with no diff base at all: deriving a |
| 305 | // merge base for the MR head is deliberately out of scope here (#172 — |
| 306 | // filtering a head down to zero jobs leaves it with no statuses, which |
| 307 | // the require_checks gate reads as unmergeable). This test documents |
| 308 | // and locks in that choice. |
| 309 | func TestQueueMRBuildsStillFailsOpen(t *testing.T) { |
| 310 | st, repo, uid := newQueueTestRepo(t) |
| 311 | git := gitRunner(t) |
| 312 | root := t.TempDir() |
| 313 | |
| 314 | src := filepath.Join(root, "src") |
| 315 | os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755) |
| 316 | os.MkdirAll(filepath.Join(src, "docs"), 0o755) |
| 317 | os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte( |
| 318 | "jobs:\n unit:\n paths-ignore:\n - docs/**\n steps:\n - echo hi\n"), 0o644) |
| 319 | os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x\n"), 0o644) |
| 320 | git(root, "init", "-q", "-b", "main", "src") |
| 321 | git(src, "add", ".") |
| 322 | git(src, "commit", "-q", "-m", "base") |
| 323 | |
| 324 | git(src, "checkout", "-q", "-b", "pr") |
| 325 | os.WriteFile(filepath.Join(src, "docs", "x.md"), []byte("# x changed\n"), 0o644) |
| 326 | git(src, "add", ".") |
| 327 | git(src, "commit", "-q", "-m", "docs only") |
| 328 | prSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD")) |
| 329 | |
| 330 | dir := RepoDir(root, repo.OwnerName, repo.Name) |
| 331 | os.MkdirAll(filepath.Dir(dir), 0o755) |
| 332 | git(root, "clone", "-q", "--bare", src, dir) |
| 333 | git(dir, "update-ref", "refs/merge-requests/1/head", prSHA) |
| 334 | |
| 335 | QueueMRBuilds(st, root, "https://x.test", repo, uid, 1, prSHA) |
| 336 | |
| 337 | builds, err := st.ListBuilds(repo.ID, 10) |
| 338 | if err != nil || len(builds) != 1 { |
| 339 | t.Fatalf("expected the MR head to fail open and queue a build: %v %v", builds, err) |
| 340 | } |
| 341 | } |