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_test.go +83
| @@ -213,3 +213,86 @@ jobs: | |||
| 213 | 213 | t.Fatal("two jobs publishing the site were accepted") | |
| 214 | 214 | } | |
| 215 | 215 | } | |
| 216 | |||
| 217 | // TestTagTriggers are the rules a release build depends on: tags have no default, | ||
| 218 | // and saying "tags:" means you said what the workflow triggers on. | ||
| 219 | func TestTagTriggers(t *testing.T) { | ||
| 220 | // Tag-only: no branch triggers, because the workflow said what it runs on. | ||
| 221 | tagOnly, problems := Parse("ci.yaml", `version: 1 | ||
| 222 | on: | ||
| 223 | push: | ||
| 224 | tags: ['v*'] | ||
| 225 | jobs: | ||
| 226 | release: | ||
| 227 | steps: | ||
| 228 | - run: make dist | ||
| 229 | artifacts: | ||
| 230 | - path: dist/* | ||
| 231 | release: true | ||
| 232 | `) | ||
| 233 | if len(problems) > 0 { | ||
| 234 | t.Fatalf("a tag-only workflow was refused: %v", problems) | ||
| 235 | } | ||
| 236 | wf := tagOnly | ||
| 237 | if len(wf.Branches) != 0 { | ||
| 238 | t.Fatalf("tag-only workflow also runs on branches: %v", wf.Branches) | ||
| 239 | } | ||
| 240 | if !wf.AppliesToTag("v1.0.0") || wf.AppliesToTag("nightly") { | ||
| 241 | t.Fatalf("tag matching is wrong for %v", wf.Tags) | ||
| 242 | } | ||
| 243 | if !wf.Jobs[0].Artifacts[0].Release { | ||
| 244 | t.Fatal("the release flag did not survive parsing") | ||
| 245 | } | ||
| 246 | |||
| 247 | // Neither list says nothing, so the default is every branch — unchanged behaviour. | ||
| 248 | plain, problems := Parse("ci.yaml", `version: 1 | ||
| 249 | jobs: | ||
| 250 | test: | ||
| 251 | steps: | ||
| 252 | - run: true | ||
| 253 | `) | ||
| 254 | if len(problems) > 0 || len(plain.Branches) != 1 || plain.Branches[0] != "*" { | ||
| 255 | t.Fatalf("a workflow with no triggers = %+v %v", plain, problems) | ||
| 256 | } | ||
| 257 | |||
| 258 | // Release artifacts on a branch workflow have no tag to attach to. | ||
| 259 | if _, problems := Parse("ci.yaml", `version: 1 | ||
| 260 | jobs: | ||
| 261 | build: | ||
| 262 | steps: | ||
| 263 | - run: true | ||
| 264 | artifacts: | ||
| 265 | - path: dist/* | ||
| 266 | release: true | ||
| 267 | `); len(problems) == 0 { | ||
| 268 | t.Fatal("release artifacts were accepted without a tag trigger") | ||
| 269 | } | ||
| 270 | |||
| 271 | // An explicit empty branch list means branches too, alongside tags. | ||
| 272 | both, problems := Parse("ci.yaml", `version: 1 | ||
| 273 | on: | ||
| 274 | push: | ||
| 275 | branches: [] | ||
| 276 | tags: ['v*'] | ||
| 277 | jobs: | ||
| 278 | build: | ||
| 279 | steps: | ||
| 280 | - run: true | ||
| 281 | `) | ||
| 282 | if len(problems) > 0 || len(both.Branches) != 0 || len(both.Tags) != 1 { | ||
| 283 | t.Fatalf("branches: [] with tags = %+v %v", both, problems) | ||
| 284 | } | ||
| 285 | |||
| 286 | // A tag pattern that is not a pattern is refused like a branch one. | ||
| 287 | if _, problems := Parse("ci.yaml", `version: 1 | ||
| 288 | on: | ||
| 289 | push: | ||
| 290 | tags: ['../x'] | ||
| 291 | jobs: | ||
| 292 | build: | ||
| 293 | steps: | ||
| 294 | - run: true | ||
| 295 | `); len(problems) == 0 { | ||
| 296 | t.Fatal("a tag pattern with .. was accepted") | ||
| 297 | } | ||
| 298 | } | ||