Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Phase 4 slice 3: checks on the gate, and statuses on the page

35eaeccffdab9d07e13fb7b346060e535697fee7 · Keenan Nemetz · 17d ago · parent b44cbfaf

Phase 4 slice 3: checks on the gate, and statuses on the page

A protected branch now requires both an approved change request and passing checks
for the commit being landed. Approvals say a person read the change; checks say it
works. Neither substitutes for the other.

The gate asks about the *approved head*, not about the merge commit somebody may be
pushing: CI ran on the head, and a merge commit is a new object nobody built. That
also keeps the gate consistent with what approvals already name.

Details that decide whether this is usable:

- A repository with no workflows is unaffected — nothing to run means nothing to
  wait for — so existing repositories do not start failing pushes.
- A job with no run is refused with an explanation that names the branch filter,
  because `on: push: branches: [main]` never produces a result for a feature
  branch. That is the one configuration that can deadlock a merge, and the error
  says so instead of leaving somebody guessing.
- Failed, cancelled, still-running and missing are four different messages; a
  reviewer should be able to tell which.
- `vole acl checks <repo> on|off` turns it off, default on.

Statuses are readable: the repository page lists recent runs (job, commit, status,
attempts, age) and a change request shows its head's job statuses next to the
approvals, so the reviewer sees the same facts the gate uses.

Tests cover the three interesting shapes: approved + green lands; approved + red is
refused naming the failed job; approved + no run at all is refused with the
branch-filter hint.

Verified on a live instance: pushing a workflow queues and runs it, the repository
page shows `build · .vole/workflows/ci.yaml · success`, and the change request shows
`wilbur · current` alongside `build · success`.

Phase 4 is now complete except signed run records, which are Phase 7's.

internal/hook/checks.go +68

@@ -0,0 +1,68 @@
1 package hook
2
3 import (
4 "fmt"
5 "strings"
6
7 "github.com/rodent-software/vole/internal/gitx"
8 "github.com/rodent-software/vole/internal/runs"
9 "github.com/rodent-software/vole/internal/workflow"
10 )
11
12 // checkGaps reports which required CI jobs have not passed for a commit.
13 //
14 // The question is asked of the commit being landed — the approved head — not of
15 // the merge commit a person may be pushing, because CI runs on what was reviewed;
16 // a merge commit is a new object nobody built.
17 func checkGaps(g *gitx.Repo, runsDB, repo, sha, branch string) ([]string, error) {
18 wfs, problems := workflow.At(g, sha)
19 if len(problems) > 0 {
20 return nil, fmt.Errorf("cannot read the workflows at %s: %s", short(sha), strings.Join(problems, "; "))
21 }
22 var required []*workflow.Workflow
23 for _, wf := range wfs {
24 if wf.Applies(branch) {
25 required = append(required, wf)
26 }
27 }
28 if len(required) == 0 {
29 return nil, nil // nothing to run, so nothing to wait for
30 }
31 if runsDB == "" {
32 return nil, fmt.Errorf("this repository has workflows but no queue is configured, so required checks can never pass")
33 }
34 store, err := runs.Open(runsDB)
35 if err != nil {
36 return nil, fmt.Errorf("cannot open the run queue: %w", err)
37 }
38 defer store.Close()
39
40 results, err := store.ForSHA(repo, sha)
41 if err != nil {
42 return nil, fmt.Errorf("cannot read run results: %w", err)
43 }
44
45 var gaps []string
46 for _, wf := range required {
47 for _, job := range wf.Jobs {
48 run := results[runs.Key(wf.Path, job.ID)]
49 switch {
50 case run == nil:
51 gaps = append(gaps, fmt.Sprintf(
52 "%s: job %q has no run for %s — push the branch and let CI run on it "+
53 "(this job filters on [%s]; a commit tested only on other branches has no result here)",
54 wf.Path, job.ID, short(sha), strings.Join(wf.Branches, " ")))
55 case run.Status == runs.StatusFailed:
56 gaps = append(gaps, fmt.Sprintf(
57 "%s: job %q failed for %s (exit %d)", wf.Path, job.ID, short(sha), run.ExitCode))
58 case run.Status == runs.StatusCanceled:
59 gaps = append(gaps, fmt.Sprintf(
60 "%s: job %q was canceled for %s", wf.Path, job.ID, short(sha)))
61 case run.Status != runs.StatusSuccess:
62 gaps = append(gaps, fmt.Sprintf(
63 "%s: job %q is %s for %s; wait for it to finish", wf.Path, job.ID, run.Status, short(sha)))
64 }
65 }
66 }
67 return gaps, nil
68 }