vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Test the merge result before landing it
e5478085e5b53a2892d0649d6f07c6f0a5653378 · Keenan Nemetz · 11d ago · parent db5e4382
Test the merge result before landing it A protected branch could receive a non-fast-forward landing whose tree was never built: the gate asked for checks on the request head, and the merge commit was made at landing time. A pushed merge with the right parents but extra content was accepted too. Publish the merge commit under refs/vole/candidates/, test it as the base branch, and make the gate require its results. Approvals name the head and the base branch, so a moved base retests without a new approval. A merger who clicks while that run is still going waits for it instead of clicking again. A pushed merge commit now has to carry exactly the tree a clean merge would produce. Closes: 01M61QQ56TESQ1HTMB1Z5C3733
server/ci.go +31−13
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "github.com/rodent-software/vole/ci/runs" | |
| 11 | 11 | "github.com/rodent-software/vole/core/id" | |
| 12 | 12 | "github.com/rodent-software/vole/forge/access" | |
| 13 | "github.com/rodent-software/vole/forge/repo" | ||
| 13 | 14 | "github.com/rodent-software/vole/metadata" | |
| 14 | 15 | ) | |
| 15 | 16 | ||
| @@ -105,13 +106,21 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 105 | 106 | annotated = kind == "tag" | |
| 106 | 107 | } | |
| 107 | 108 | } | |
| 109 | s.queue(m, ev.Repo, ev.Ref, branch, tag, commit, annotated, true) | ||
| 110 | } | ||
| 111 | |||
| 112 | // queue pins and enqueues the jobs a commit owes. The two callers are a push and the | ||
| 113 | // combined-result reconciler; they differ in the branch a workflow filter sees, in | ||
| 114 | // whether a tag makes this a release, and in whether the job may deploy the site. | ||
| 115 | // Nothing else about a run depends on how it was requested. | ||
| 116 | func (s *Server) queue(m *repo.Meta, repoName, ref, branch, tag, commit string, annotated, deployPages bool) int { | ||
| 108 | 117 | wfs, problems := ci.At(m.Git(), commit) | |
| 109 | s.Log.Debug("ci: examined push", "repo", ev.Repo, "ref", ev.Ref, "workflows", len(wfs), "problems", len(problems)) | ||
| 118 | s.Log.Debug("ci: examined commit", "repo", repoName, "ref", ref, "commit", commit, "workflows", len(wfs), "problems", len(problems)) | ||
| 110 | 119 | if len(problems) > 0 { | |
| 111 | 120 | // pre-receive rejects these, so reaching here means a workflow was already | |
| 112 | 121 | // broken: queue nothing rather than guess. | |
| 113 | s.Log.Warn("ci: workflows in a pushed commit do not validate", "repo", ev.Repo, "problems", len(problems)) | ||
| 114 | return | ||
| 122 | s.Log.Warn("ci: workflows in a pushed commit do not validate", "repo", repoName, "problems", len(problems)) | ||
| 123 | return 0 | ||
| 115 | 124 | } | |
| 116 | 125 | // What this push owes: one run per job of every workflow that applies to it. The | |
| 117 | 126 | // list is built before anything is queued, because a push that owes nothing must | |
| @@ -131,9 +140,13 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 131 | 140 | continue | |
| 132 | 141 | } | |
| 133 | 142 | for _, job := range wf.Jobs { | |
| 143 | site := "" | ||
| 144 | if deployPages { | ||
| 145 | site = pagesPath(job) | ||
| 146 | } | ||
| 134 | 147 | run := runs.Run{ | |
| 135 | Repo: ev.Repo, | ||
| 136 | Ref: ev.Ref, | ||
| 148 | Repo: repoName, | ||
| 149 | Ref: ref, | ||
| 137 | 150 | Branch: branch, | |
| 138 | 151 | Tag: tag, | |
| 139 | 152 | TagAnnotated: annotated, | |
| @@ -149,7 +162,7 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 149 | 162 | Artifacts: artifactPaths(job), | |
| 150 | 163 | Env: job.Env, | |
| 151 | 164 | ReleaseArtifacts: releasePaths(job), | |
| 152 | PagesPath: pagesPath(job), | ||
| 165 | PagesPath: site, | ||
| 153 | 166 | Created: time.Now().UTC(), | |
| 154 | 167 | } | |
| 155 | 168 | run.ID, _ = id.New(time.Now()) | |
| @@ -157,28 +170,33 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 157 | 170 | } | |
| 158 | 171 | } | |
| 159 | 172 | if len(owing) == 0 { | |
| 160 | return | ||
| 173 | return 0 | ||
| 161 | 174 | } | |
| 162 | 175 | // A newer tip supersedes the queued builds of the older one: pushing twice in a | |
| 163 | 176 | // minute must not build the first commit twice. Running jobs are left alone, and | |
| 164 | 177 | // the cancellation is logged because a run that stopped for a reason nobody can | |
| 165 | // find is the same as a run that vanished. | ||
| 166 | if n, err := s.Runs.CancelSuperseded(ev.Repo, ev.Ref, commit, time.Now().UTC()); err != nil { | ||
| 167 | s.Log.Warn("ci: cannot cancel superseded runs", "repo", ev.Repo, "ref", ev.Ref, "err", err) | ||
| 178 | // find is the same as a run that vanished. A candidate ref names one commit, so | ||
| 179 | // this is a no-op for combined results; the cancellation that matters there is | ||
| 180 | // the stale-ref cleanup. | ||
| 181 | if n, err := s.Runs.CancelSuperseded(repoName, ref, commit, time.Now().UTC()); err != nil { | ||
| 182 | s.Log.Warn("ci: cannot cancel superseded runs", "repo", repoName, "ref", ref, "err", err) | ||
| 168 | 183 | } else if n > 0 { | |
| 169 | s.Log.Info("ci: canceled superseded runs", "repo", ev.Repo, "ref", ev.Ref, "runs", n) | ||
| 184 | s.Log.Info("ci: canceled superseded runs", "repo", repoName, "ref", ref, "runs", n) | ||
| 170 | 185 | } | |
| 186 | queued := 0 | ||
| 171 | 187 | for _, owed := range owing { | |
| 172 | 188 | run := owed.run | |
| 173 | 189 | created, err := s.Runs.Enqueue(run, owed.labels) | |
| 174 | 190 | if err != nil { | |
| 175 | s.Log.Warn("ci: cannot queue a run", "repo", ev.Repo, "job", run.Job, "err", err) | ||
| 191 | s.Log.Warn("ci: cannot queue a run", "repo", repoName, "job", run.Job, "err", err) | ||
| 176 | 192 | continue | |
| 177 | 193 | } | |
| 178 | 194 | if created { | |
| 179 | s.Log.Info("ci: queued", "repo", ev.Repo, "job", run.Job, "sha", commit[:8], "run", run.ID) | ||
| 195 | queued++ | ||
| 196 | s.Log.Info("ci: queued", "repo", repoName, "job", run.Job, "sha", run.SHA[:8], "run", run.ID) | ||
| 180 | 197 | } | |
| 181 | 198 | } | |
| 199 | return queued | ||
| 182 | 200 | } | |
| 183 | 201 | ||
| 184 | 202 | // reconcileSeconds is how often the queue is checked against the refs. The spool | |