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 | } | ||