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.

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)