Mail the repo's notify targets when a build fails !60
2 files changed, +59 −0
Layout: unified · split
e2e/ci_test.go +43
| @@ -38,6 +38,49 @@ func (i *instance) runnerOnce(t *testing.T, key string) string { | ||
| 38 | 38 | return string(out) |
| 39 | 39 | } |
| 40 | 40 | |
| 41 | // A failed build mails the repo owner with the log tail; green builds | |
| 42 | // stay silent. | |
| 43 | func TestBuildFailureMail(t *testing.T) { | |
| 44 | smtp := startFakeSMTP(t) | |
| 45 | inst := startInstanceWith(t, fmt.Sprintf( | |
| 46 | "[mail]\nsmtp_host = %q\nfrom = \"noreply@gitbay.test\"\n", smtp.addr)) | |
| 47 | inst.runner = buildRunner(t) | |
| 48 | aliceKey := inst.newKey(t, "alice") | |
| 49 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", | |
| 50 | "--email", "alice@example.test", "--verified") | |
| 51 | runnerKey := inst.newKey(t, "ci") | |
| 52 | inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin") | |
| 53 | ||
| 54 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 { | |
| 55 | t.Fatalf("repo create: %s", errOut) | |
| 56 | } | |
| 57 | work := t.TempDir() | |
| 58 | env := inst.gitEnv(aliceKey) | |
| 59 | mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w") | |
| 60 | dir := filepath.Join(work, "w") | |
| 61 | os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755) | |
| 62 | os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte( | |
| 63 | "jobs:\n ok:\n steps:\n - echo fine\n broken:\n steps:\n - echo the dataset went stale && false\n"), 0o644) | |
| 64 | mustGit(t, dir, env, "checkout", "-q", "-b", "main") | |
| 65 | mustGit(t, dir, env, "add", ".") | |
| 66 | mustGit(t, dir, env, "commit", "-q", "-m", "base") | |
| 67 | mustGit(t, dir, env, "push", "-q", "origin", "main") | |
| 68 | ||
| 69 | inst.runnerOnce(t, runnerKey) // broken (sorts first) | |
| 70 | inst.runnerOnce(t, runnerKey) // ok | |
| 71 | mail := smtp.waitFor(t, "alice@example.test", "failed") | |
| 72 | if !strings.Contains(mail, "broken") || !strings.Contains(mail, "the dataset went stale") || | |
| 73 | !strings.Contains(mail, "/alice/app/builds/") { | |
| 74 | t.Fatalf("failure mail missing detail:\n%s", mail) | |
| 75 | } | |
| 76 | // Only the failure mailed: no message mentions the green job. | |
| 77 | for _, m := range smtp.mailTo("alice@example.test") { | |
| 78 | if strings.Contains(m, "build") && strings.Contains(m, " ok ") && strings.Contains(m, "failed") == false { | |
| 79 | t.Fatalf("green build mailed:\n%s", m) | |
| 80 | } | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 41 | 84 | func TestCI(t *testing.T) { |
| 42 | 85 | inst := startInstance(t) |
| 43 | 86 | inst.runner = buildRunner(t) |
internal/control/build.go +16
| @@ -340,6 +340,22 @@ func runRunnerDone(c *Ctx, args []string) int { | ||
| 340 | 340 | } |
| 341 | 341 | c.Store.RecordEvent(repo.ID, c.User.ID, "build."+args[1], |
| 342 | 342 | fmt.Sprintf(`{"number":%d,"job":%q}`, b.Number, b.Job)) |
| 343 | // A red build mails the repo's notify targets with the log tail — a | |
| 344 | // failed scheduled job must not wait to be noticed. | |
| 345 | if args[1] == "failure" { | |
| 346 | if targets, err := c.Store.RepoNotifyTargets(repo); err == nil { | |
| 347 | tail := "" | |
| 348 | if log, err := c.Store.BuildLog(id); err == nil && len(log) > 0 { | |
| 349 | if len(log) > 2000 { | |
| 350 | log = log[len(log)-2000:] | |
| 351 | } | |
| 352 | tail = string(log) | |
| 353 | } | |
| 354 | notifyUsers(c, targets, | |
| 355 | fmt.Sprintf("[%s] build %d failed: %s on %s", repo.Path(), b.Number, b.Job, b.Ref), | |
| 356 | fmt.Sprintf("job %s failed at %.10s.\n\n…%s\n\n%s\n", b.Job, b.SHA, tail, url)) | |
| 357 | } | |
| 358 | } | |
| 343 | 359 | return c.emit(map[string]any{"build": b.Number, "status": args[1]}, func(w io.Writer) { |
| 344 | 360 | fmt.Fprintf(w, "build %d %s\n", b.Number, args[1]) |
| 345 | 361 | }) |