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.
Phase 7 slice 8: a tag push builds the release
0f442b1c87ca3920f770531c9ad97277d0231f51 · Keenan Nemetz · 17d ago · parent 88e980b0
Phase 7 slice 8: a tag push builds the release on.push.tags opens CI to tag pushes; release: true on an artifact publishes it to that tag's release rather than only to the run. The runner commits the same pointers `vole release attach` writes, so the release page, the ETags, the immutability rules and the sweep needed no changes: CI gained a trigger, not a publish path. Three rules, each from something that went wrong in the first draft: - a workflow that says tags: runs ONLY on tags. Saying what you trigger on is saying it; the first version defaulted to every branch too and queued a doomed release build on every commit - a tag run never counts as a branch's required check. A tag on a branch tip produces a second run for the same sha, and ForSHA would have let a release build satisfy — or break — the gate for the commit it tags. It now filters to refs/heads/% - a lightweight tag cannot name a release: the build runs and the publish fails saying so, because a tag with no message has no notes and no tagger. The server resolves annotated-ness at enqueue (the tag object's type) rather than having the runner parse ls-remote peeled lines Tag and annotated-ness ride the run and the job payload beside the release artifact patterns, pinned at enqueue like everything else. Measured through the binary: a branch push queued nothing, a tag push queued the release job, the runner published dist/tool-linux, the release page listed it with the tag marked annotated, the download returned the bytes, and git log vole read "release: v1.0.0 artifacts from run 01M5MSX3...".
internal/workflow/workflow.go +47−6
| @@ -59,6 +59,10 @@ type Step struct { | |||
| 59 | 59 | // push, while a shell command would fail at run time, possibly much later. | |
| 60 | 60 | type Artifact struct { | |
| 61 | 61 | Path string | |
| 62 | // Release publishes the matches as artifacts of the tag's release instead of | ||
| 63 | // only as this run's outputs. It requires a tag-triggered workflow: a branch run | ||
| 64 | // has no tag to attach to. | ||
| 65 | Release bool | ||
| 62 | 66 | } | |
| 63 | 67 | ||
| 64 | 68 | type Job struct { | |
| @@ -85,7 +89,20 @@ type Workflow struct { | |||
| 85 | 89 | BlobSHA string | |
| 86 | 90 | Version int | |
| 87 | 91 | Branches []string | |
| 88 | Jobs []Job | ||
| 92 | // Tags are the tag patterns that trigger this workflow. A tag run is how a | ||
| 93 | // release build happens: the tag names what is being released. | ||
| 94 | Tags []string | ||
| 95 | Jobs []Job | ||
| 96 | } | ||
| 97 | |||
| 98 | // AppliesToTag reports whether this workflow should run for a tag push. | ||
| 99 | func (w *Workflow) AppliesToTag(tag string) bool { | ||
| 100 | for _, pattern := range w.Tags { | ||
| 101 | if ok, err := path.Match(pattern, tag); err == nil && ok { | ||
| 102 | return true | ||
| 103 | } | ||
| 104 | } | ||
| 105 | return false | ||
| 89 | 106 | } | |
| 90 | 107 | ||
| 91 | 108 | // Applies reports whether this workflow should run for a push to branch. | |
| @@ -105,7 +122,8 @@ type rawStep struct { | |||
| 105 | 122 | } | |
| 106 | 123 | ||
| 107 | 124 | type rawArtifact struct { | |
| 108 | Path string `yaml:"path"` | ||
| 125 | Path string `yaml:"path"` | ||
| 126 | Release bool `yaml:"release"` | ||
| 109 | 127 | } | |
| 110 | 128 | ||
| 111 | 129 | type rawPages struct { | |
| @@ -122,7 +140,11 @@ type rawWorkflow struct { | |||
| 122 | 140 | Version int `yaml:"version"` | |
| 123 | 141 | On struct { | |
| 124 | 142 | Push struct { | |
| 125 | Branches []string `yaml:"branches"` | ||
| 143 | // Pointers, so "absent" and "empty" stay different: a workflow with tags | ||
| 144 | // and no branches runs only on tags, while one with neither runs on every | ||
| 145 | // branch. | ||
| 146 | Branches *[]string `yaml:"branches"` | ||
| 147 | Tags *[]string `yaml:"tags"` | ||
| 126 | 148 | } `yaml:"push"` | |
| 127 | 149 | } `yaml:"on"` | |
| 128 | 150 | Jobs map[string]rawJob `yaml:"jobs"` | |
| @@ -159,8 +181,12 @@ func Parse(file string, content string) (*Workflow, []string) { | |||
| 159 | 181 | } | |
| 160 | 182 | ||
| 161 | 183 | wf := &Workflow{Path: file, Version: rw.Version} | |
| 162 | wf.Branches = rw.On.Push.Branches | ||
| 163 | if len(wf.Branches) == 0 { | ||
| 184 | switch { | ||
| 185 | case rw.On.Push.Branches != nil: | ||
| 186 | wf.Branches = *rw.On.Push.Branches | ||
| 187 | case rw.On.Push.Tags == nil: | ||
| 188 | // Nothing was said, so this workflow is an ordinary push workflow. Saying | ||
| 189 | // "tags:" is saying what it triggers on, so it does not also mean branches. | ||
| 164 | 190 | wf.Branches = []string{"*"} | |
| 165 | 191 | } | |
| 166 | 192 | for _, b := range wf.Branches { | |
| @@ -168,6 +194,16 @@ func Parse(file string, content string) (*Workflow, []string) { | |||
| 168 | 194 | bad("on.push.branches: %q is not a branch pattern", b) | |
| 169 | 195 | } | |
| 170 | 196 | } | |
| 197 | // Tags have no default: a workflow that runs on a tag push has to say so, or a | ||
| 198 | // release build would fire on every push. | ||
| 199 | if rw.On.Push.Tags != nil { | ||
| 200 | for _, tg := range *rw.On.Push.Tags { | ||
| 201 | if strings.TrimSpace(tg) == "" || !branchGlob.MatchString(tg) || strings.Contains(tg, "..") { | ||
| 202 | bad("on.push.tags: %q is not a tag pattern", tg) | ||
| 203 | } | ||
| 204 | wf.Tags = append(wf.Tags, tg) | ||
| 205 | } | ||
| 206 | } | ||
| 171 | 207 | ||
| 172 | 208 | if len(rw.Jobs) == 0 { | |
| 173 | 209 | bad("at least one job is required") | |
| @@ -257,7 +293,12 @@ func Parse(file string, content string) (*Workflow, []string) { | |||
| 257 | 293 | bad("%s: %q is not a glob", where, p) | |
| 258 | 294 | continue | |
| 259 | 295 | } | |
| 260 | job.Artifacts = append(job.Artifacts, Artifact{Path: p}) | ||
| 296 | if ar.Release && len(wf.Tags) == 0 { | ||
| 297 | bad("%s: release artifacts need a tag trigger; add on.push.tags so the build "+ | ||
| 298 | "knows which release it is publishing", where) | ||
| 299 | continue | ||
| 300 | } | ||
| 301 | job.Artifacts = append(job.Artifacts, Artifact{Path: p, Release: ar.Release}) | ||
| 261 | 302 | } | |
| 262 | 303 | if jr.Pages != nil { | |
| 263 | 304 | where := fmt.Sprintf("jobs.%s.pages.path", id) | |