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.
Add event, schedule and manual workflow triggers
eb07ff5696e0d83201215966e3ea1e6a24467581 · Keenan Nemetz · 8d ago · parent 1cde8b9e
Add event, schedule and manual workflow triggers Workflows can now react to change requests, issues, comments, cron schedules and manual runs, and a push trigger can filter by the paths it changed. Branch and tag patterns gain ! exclusion. Event runs are pinned with a JSON payload and the runner exposes it as VOLE_EVENT, VOLE_EVENT_ACTION and VOLE_EVENT_PATH. Change-request runs check out the base tip and carry the head and changed files, so an event never executes an unapproved contributor commit. Workflow dispatch resolves typed inputs and is available from the workflows page and `vole workflow dispatch`. Deduplication now includes an occurrence so the same commit can run for a later event or schedule slot while a push remains idempotent. Part of: #138
server/ci.go +101−27
| @@ -3,11 +3,13 @@ package server | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | "sort" | ||
| 6 | 7 | "strings" | |
| 7 | 8 | "time" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/rodent-software/vole/ci" | |
| 10 | 11 | "github.com/rodent-software/vole/ci/runs" | |
| 12 | "github.com/rodent-software/vole/core/git" | ||
| 11 | 13 | "github.com/rodent-software/vole/core/id" | |
| 12 | 14 | "github.com/rodent-software/vole/forge/access" | |
| 13 | 15 | "github.com/rodent-software/vole/forge/repo" | |
| @@ -55,6 +57,10 @@ func (s *Server) watchSpool(ctx context.Context) { | |||
| 55 | 57 | // opts into it with on.push.tags. A tag has no branch, and a branch has no tag, which | |
| 56 | 58 | // is why the two are carried separately rather than as one "ref". | |
| 57 | 59 | func (s *Server) enqueue(ev runs.Event) { | |
| 60 | if ev.Ref == metadata.Ref { | ||
| 61 | s.processMetadataRange(ev.Repo, ev.Old, ev.New) | ||
| 62 | return | ||
| 63 | } | ||
| 58 | 64 | var branch, tag string | |
| 59 | 65 | switch { | |
| 60 | 66 | case strings.HasPrefix(ev.Ref, "refs/heads/"): | |
| @@ -106,24 +112,56 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 106 | 112 | annotated = kind == "tag" | |
| 107 | 113 | } | |
| 108 | 114 | } | |
| 109 | s.queue(m, ev.Repo, ev.Ref, branch, tag, commit, annotated, true) | ||
| 115 | changed, err := changedPaths(m.Git(), ev.Old, commit) | ||
| 116 | if err != nil { | ||
| 117 | s.Log.Warn("ci: cannot read the paths a push changed", "repo", ev.Repo, "ref", ev.Ref, "err", err) | ||
| 118 | return | ||
| 119 | } | ||
| 120 | s.queue(m, ev.Repo, queueSpec{ | ||
| 121 | ref: ev.Ref, branch: branch, tag: tag, commit: commit, annotated: annotated, | ||
| 122 | deployPages: true, push: true, cancelSuperseded: true, | ||
| 123 | trigger: "push", action: "pushed", changed: changed, | ||
| 124 | }) | ||
| 110 | 125 | } | |
| 111 | 126 | ||
| 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 { | ||
| 117 | wfs, problems := ci.At(m.Git(), commit) | ||
| 118 | s.Log.Debug("ci: examined commit", "repo", repoName, "ref", ref, "commit", commit, "workflows", len(wfs), "problems", len(problems)) | ||
| 127 | // queueSpec is one request to queue the jobs a commit owes. A push and an event | ||
| 128 | // differ in what selects a workflow and in the event pinned with each run; nothing | ||
| 129 | // else about a run depends on how it was requested. | ||
| 130 | type queueSpec struct { | ||
| 131 | ref string | ||
| 132 | branch string | ||
| 133 | tag string | ||
| 134 | commit string | ||
| 135 | annotated bool | ||
| 136 | // deployPages says whether a job that declares pages publishes the site. A push | ||
| 137 | // does; a combined result does not, and a metadata event does only on its base | ||
| 138 | // branch. | ||
| 139 | deployPages bool | ||
| 140 | // push selects push triggers (branch, tag, path filters). When false, trigger | ||
| 141 | // and action select an event trigger. | ||
| 142 | push bool | ||
| 143 | trigger string | ||
| 144 | action string | ||
| 145 | payload string | ||
| 146 | occurrence string | ||
| 147 | changed []string | ||
| 148 | cancelSuperseded bool | ||
| 149 | } | ||
| 150 | |||
| 151 | // queue pins and enqueues the jobs a commit owes. The callers are a push, the | ||
| 152 | // combined-result reconciler, and the event triggers; they differ in what selects a | ||
| 153 | // workflow and in the event pinned with the run. | ||
| 154 | func (s *Server) queue(m *repo.Meta, repoName string, spec queueSpec) int { | ||
| 155 | wfs, problems := ci.At(m.Git(), spec.commit) | ||
| 156 | s.Log.Debug("ci: examined commit", "repo", repoName, "ref", spec.ref, "commit", spec.commit, "workflows", len(wfs), "problems", len(problems)) | ||
| 119 | 157 | if len(problems) > 0 { | |
| 120 | 158 | // pre-receive rejects these, so reaching here means a workflow was already | |
| 121 | 159 | // broken: queue nothing rather than guess. | |
| 122 | 160 | s.Log.Warn("ci: workflows in a pushed commit do not validate", "repo", repoName, "problems", len(problems)) | |
| 123 | 161 | return 0 | |
| 124 | 162 | } | |
| 125 | // What this push owes: one run per job of every workflow that applies to it. The | ||
| 126 | // list is built before anything is queued, because a push that owes nothing must | ||
| 163 | // What this event owes: one run per job of every workflow that applies to it. The | ||
| 164 | // list is built before anything is queued, because an event that owes nothing must | ||
| 127 | 165 | // not cancel the previous tip's builds. A job's runs-on labels travel beside the | |
| 128 | 166 | // run rather than inside it: they are lease policy, and Enqueue takes them as its | |
| 129 | 167 | // own argument so a caller holding the job cannot forget them. | |
| @@ -133,28 +171,33 @@ func (s *Server) queue(m *repo.Meta, repoName, ref, branch, tag, commit string, | |||
| 133 | 171 | } | |
| 134 | 172 | var owing []owedRun | |
| 135 | 173 | for _, wf := range wfs { | |
| 136 | switch { | ||
| 137 | case branch != "" && !wf.Applies(branch): | ||
| 138 | continue | ||
| 139 | case tag != "" && !wf.AppliesToTag(tag): | ||
| 174 | if spec.push { | ||
| 175 | if !wf.AppliesToPush(spec.branch, spec.tag, spec.changed) { | ||
| 176 | continue | ||
| 177 | } | ||
| 178 | } else if !wf.AppliesToEvent(spec.trigger, spec.action) { | ||
| 140 | 179 | continue | |
| 141 | 180 | } | |
| 142 | 181 | for _, job := range wf.Jobs { | |
| 143 | 182 | site := "" | |
| 144 | if deployPages { | ||
| 183 | if spec.deployPages { | ||
| 145 | 184 | site = pagesPath(job) | |
| 146 | 185 | } | |
| 147 | 186 | run := runs.Run{ | |
| 148 | 187 | Repo: repoName, | |
| 149 | Ref: ref, | ||
| 150 | Branch: branch, | ||
| 151 | Tag: tag, | ||
| 152 | TagAnnotated: annotated, | ||
| 153 | SHA: commit, | ||
| 188 | Ref: spec.ref, | ||
| 189 | Branch: spec.branch, | ||
| 190 | Tag: spec.tag, | ||
| 191 | TagAnnotated: spec.annotated, | ||
| 192 | SHA: spec.commit, | ||
| 154 | 193 | Workflow: wf.Path, | |
| 155 | 194 | WorkflowSHA: wf.BlobSHA, | |
| 156 | 195 | WorkflowName: wf.Name, | |
| 157 | 196 | WorkflowDescription: wf.Description, | |
| 197 | Trigger: spec.trigger, | ||
| 198 | TriggerAction: spec.action, | ||
| 199 | EventPayload: spec.payload, | ||
| 200 | Occurrence: spec.occurrence, | ||
| 158 | 201 | Job: job.ID, | |
| 159 | 202 | JobName: job.Name, | |
| 160 | 203 | JobDescription: job.Description, | |
| @@ -180,12 +223,13 @@ func (s *Server) queue(m *repo.Meta, repoName, ref, branch, tag, commit string, | |||
| 180 | 223 | // minute must not build the first commit twice. Running jobs are left alone, and | |
| 181 | 224 | // the cancellation is logged because a run that stopped for a reason nobody can | |
| 182 | 225 | // find is the same as a run that vanished. A candidate ref names one commit, so | |
| 183 | // this is a no-op for combined results; the cancellation that matters there is | ||
| 184 | // the stale-ref cleanup. | ||
| 185 | if n, err := s.Runs.CancelSuperseded(repoName, ref, commit, time.Now().UTC()); err != nil { | ||
| 186 | s.Log.Warn("ci: cannot cancel superseded runs", "repo", repoName, "ref", ref, "err", err) | ||
| 187 | } else if n > 0 { | ||
| 188 | s.Log.Info("ci: canceled superseded runs", "repo", repoName, "ref", ref, "runs", n) | ||
| 226 | // this is a no-op for combined results; a metadata event never cancels anything. | ||
| 227 | if spec.cancelSuperseded { | ||
| 228 | if n, err := s.Runs.CancelSuperseded(repoName, spec.ref, spec.commit, time.Now().UTC()); err != nil { | ||
| 229 | s.Log.Warn("ci: cannot cancel superseded runs", "repo", repoName, "ref", spec.ref, "err", err) | ||
| 230 | } else if n > 0 { | ||
| 231 | s.Log.Info("ci: canceled superseded runs", "repo", repoName, "ref", spec.ref, "runs", n) | ||
| 232 | } | ||
| 189 | 233 | } | |
| 190 | 234 | queued := 0 | |
| 191 | 235 | for _, owed := range owing { | |
| @@ -197,12 +241,42 @@ func (s *Server) queue(m *repo.Meta, repoName, ref, branch, tag, commit string, | |||
| 197 | 241 | } | |
| 198 | 242 | if created { | |
| 199 | 243 | queued++ | |
| 200 | s.Log.Info("ci: queued", "repo", repoName, "job", run.Job, "sha", run.SHA[:8], "run", run.ID) | ||
| 244 | s.Log.Info("ci: queued", "repo", repoName, "job", run.Job, "sha", run.SHA[:8], "run", run.ID, "trigger", run.Trigger) | ||
| 201 | 245 | } | |
| 202 | 246 | } | |
| 203 | 247 | return queued | |
| 204 | 248 | } | |
| 205 | 249 | ||
| 250 | // changedPaths lists the paths a ref update touched. A new ref has no old tree, so | ||
| 251 | // every path in the new commit is new; an existing ref is compared tree to tree, so | ||
| 252 | // a force-push is measured by what actually differs. | ||
| 253 | func changedPaths(g *git.Repo, old, new string) ([]string, error) { | ||
| 254 | if new == "" || new == git.ZeroSHA { | ||
| 255 | return nil, nil | ||
| 256 | } | ||
| 257 | if old == "" || old == git.ZeroSHA { | ||
| 258 | entries, err := g.LsTreeR(new) | ||
| 259 | if err != nil { | ||
| 260 | return nil, err | ||
| 261 | } | ||
| 262 | paths := make([]string, 0, len(entries)) | ||
| 263 | for _, entry := range entries { | ||
| 264 | paths = append(paths, entry.Name) | ||
| 265 | } | ||
| 266 | return paths, nil | ||
| 267 | } | ||
| 268 | status, err := g.DiffStatus(old, new) | ||
| 269 | if err != nil { | ||
| 270 | return nil, err | ||
| 271 | } | ||
| 272 | paths := make([]string, 0, len(status)) | ||
| 273 | for p := range status { | ||
| 274 | paths = append(paths, p) | ||
| 275 | } | ||
| 276 | sort.Strings(paths) | ||
| 277 | return paths, nil | ||
| 278 | } | ||
| 279 | |||
| 206 | 280 | // reconcileSeconds is how often the queue is checked against the refs. The spool | |
| 207 | 281 | // delivers a push in well under a second; this is the safety net that makes the | |
| 208 | 282 | // promise eventually true when the spool file was never written. | |