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...".
docs/roadmap.md +20
| @@ -519,6 +519,23 @@ the shared push helper was mistaken for a missing *named* remote, so a runner's | |||
| 519 | 519 | committed locally and reported success — the end-to-end test caught it, because the | |
| 520 | 520 | site served a 404 after a "successful" run.) | |
| 521 | 521 | ||
| 522 | ✅ **Slice 8 — a tag push builds the release.** `on.push.tags` opens CI to tag pushes, | ||
| 523 | and `release: true` on an artifact publishes it to *that tag's release* rather than only | ||
| 524 | to the run. The runner does exactly what `vole release attach` does — commits the same | ||
| 525 | pointers on `refs/heads/vole` — so the release page, the ETags, the immutability rules | ||
| 526 | and the sweep needed no changes at all. Three rules earned their place. A workflow that | ||
| 527 | says `tags:` runs only on tags: naming your trigger is naming it, and the alternative | ||
| 528 | queued a release build on every commit (measured: a branch push to a tag-only workflow | ||
| 529 | queued zero runs). A tag run never counts as a branch's required check, even when the | ||
| 530 | tag points at that branch's tip — `ForSHA` now filters to `refs/heads/%`, or a release | ||
| 531 | build could satisfy or break the gate for the commit it tags. A lightweight tag cannot | ||
| 532 | name a release: the build runs and the publish fails saying so, because a tag with no | ||
| 533 | message has no notes and no tagger. | ||
| 534 | (Measured through the binary: a branch push queued nothing, a tag push queued the | ||
| 535 | release job, the runner published `dist/tool-linux`, the release page listed it with the | ||
| 536 | tag marked annotated, the download returned the bytes, and `git log vole` read | ||
| 537 | `release: v1.0.0 artifacts from run 01M5MSX3...`.) | ||
| 538 | |||
| 522 | 539 | Planned: | |
| 523 | 540 | ||
| 524 | 541 | - Identity without ceremony: `vole whoami` resolves the key a person pushes with to an | |
| @@ -693,6 +710,9 @@ Filled in as phases land. Append; don't rewrite. | |||
| 693 | 710 | | 2026-10-22 | No release record file, and no drafts | The artifact list is the pointer directory and published-ness is the ref itself; a `releases/<tag>.json` would carry a flag that duplicates the tag and an edit history nobody asked for | | |
| 694 | 711 | | 2026-10-22 | Attach uploads bytes before it commits the pointer | The reverse order publishes a download that 404s; the hook cannot see bytes that were never in the pack, so the pointer check is the only guard and the GC grace period covers the gap | | |
| 695 | 712 | | 2026-10-22 | Artifact downloads are always an attachment, served through `http.ServeContent` | An uploaded `.html` rendered inline is stored XSS on the forge origin; `ServeContent` gives `Range` and conditional requests for free, and the LFS object id is the validator | | |
| 713 | | 2026-10-23 | Saying `on.push.tags` means tags only | A workflow that named its trigger still firing on every branch queued a doomed release build per commit; "no triggers at all" keeps the existing every-branch default | | ||
| 714 | | 2026-10-23 | A tag run never decides a branch's required checks | A tag on a branch tip produces a second run for the same sha; counting it would let a release build satisfy — or break — the gate for the commit it tags | | ||
| 715 | | 2026-10-23 | CI release artifacts are the same pointers a person commits | The release page, ETags, immutability and the sweep already read that shape, so CI needed no new publish path — only the run id in the commit message | | ||
| 696 | 716 | | 2026-10-23 | CI deploys the site through the CLI's own snapshot code | A site published by a workflow and one published by a person must be the same commit shape; a second implementation would drift, and the version that drifts is the one nobody runs by hand | | |
| 697 | 717 | | 2026-10-23 | A CI site deploy is refused when the run is not the branch tip | Deploy is a snapshot, so a stale build finishing late silently reverts the site; a failed job with a reason is the honest outcome | | |
| 698 | 718 | | 2026-10-23 | One job per workflow may publish the site | Two publishers race and the winner is whichever runner finished last, which is not a decision anyone made | | |