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