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/web/web.go +10−2
| @@ -21,18 +21,20 @@ import ( | |||
| 21 | 21 | "github.com/rodent-software/vole/internal/identity" | |
| 22 | 22 | "github.com/rodent-software/vole/internal/index" | |
| 23 | 23 | "github.com/rodent-software/vole/internal/repo" | |
| 24 | "github.com/rodent-software/vole/internal/runs" | ||
| 24 | 25 | ) | |
| 25 | 26 | ||
| 26 | 27 | type Web struct { | |
| 27 | 28 | Repos *repo.Store | |
| 28 | 29 | Index *index.Store | |
| 29 | 30 | Ident *identity.Store | |
| 31 | Runs *runs.Store | ||
| 30 | 32 | Cfg *config.Config | |
| 31 | 33 | Tmpl *template.Template | |
| 32 | 34 | Logger func(format string, args ...any) | |
| 33 | 35 | } | |
| 34 | 36 | ||
| 35 | func New(repos *repo.Store, ix *index.Store, ident *identity.Store, cfg *config.Config) (*Web, error) { | ||
| 37 | func New(repos *repo.Store, ix *index.Store, ident *identity.Store, rs *runs.Store, cfg *config.Config) (*Web, error) { | ||
| 36 | 38 | t, err := template.New("vole").Funcs(template.FuncMap{ | |
| 37 | 39 | "since": humanSince, | |
| 38 | 40 | "short": shortSHA, | |
| @@ -41,7 +43,7 @@ func New(repos *repo.Store, ix *index.Store, ident *identity.Store, cfg *config. | |||
| 41 | 43 | if err != nil { | |
| 42 | 44 | return nil, err | |
| 43 | 45 | } | |
| 44 | w := &Web{Repos: repos, Index: ix, Ident: ident, Cfg: cfg, Tmpl: t} | ||
| 46 | w := &Web{Repos: repos, Index: ix, Ident: ident, Runs: rs, Cfg: cfg, Tmpl: t} | ||
| 45 | 47 | w.Logger = func(string, ...any) {} | |
| 46 | 48 | return w, nil | |
| 47 | 49 | } | |
| @@ -132,6 +134,10 @@ func (w *Web) Repo(rw http.ResponseWriter, r *http.Request) { | |||
| 132 | 134 | if err := w.Index.EnsureIssues(m); err == nil { | |
| 133 | 135 | issueCount, _ = w.Index.IssueCount(m.ID) | |
| 134 | 136 | } | |
| 137 | var recentRuns []*runs.Run | ||
| 138 | if w.Runs != nil { | ||
| 139 | recentRuns, _ = w.Runs.Recent(m.Name, 8) | ||
| 140 | } | ||
| 135 | 141 | branches, _ := g.ListRefs("refs/heads/") | |
| 136 | 142 | names := make([]string, 0, len(branches)) | |
| 137 | 143 | for k := range branches { | |
| @@ -150,6 +156,7 @@ func (w *Web) Repo(rw http.ResponseWriter, r *http.Request) { | |||
| 150 | 156 | Count int | |
| 151 | 157 | Branches []string | |
| 152 | 158 | IssueCount int | |
| 159 | Runs []*runs.Run | ||
| 153 | 160 | }{ | |
| 154 | 161 | Page: w.base(m.Name), | |
| 155 | 162 | Name: m.Name, | |
| @@ -163,6 +170,7 @@ func (w *Web) Repo(rw http.ResponseWriter, r *http.Request) { | |||
| 163 | 170 | Count: count, | |
| 164 | 171 | Branches: names, | |
| 165 | 172 | IssueCount: issueCount, | |
| 173 | Runs: recentRuns, | ||
| 166 | 174 | }, 0) | |
| 167 | 175 | } | |
| 168 | 176 | ||