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.
Phase 7 slice 5: pages are a build directory you can publish
4c0f2aefc52a5b5f20555e1698858f0a540ad8e3 · Keenan Nemetz · 17d ago · parent 948a6d7d
Phase 7 slice 5: pages are a build directory you can publish A site is a branch, so managing one is managing a branch; the sugar is worth it for one reason: a build directory is a snapshot, and a plain push from a branch leaves deleted pages served forever. - vole pages deploy [<dir>] replaces the branch's tree with the directory, as a normal commit on the tip, so a protected pages branch still goes through review and a lost push race re-applies the snapshot instead of merging two builds - --dry-run reports added/changed/removed; a deploy whose files already match the published tree pushes nothing, because a build that reruns on every commit must not add an empty commit each time - vole pages status reports tip, commit, file count and bytes; unpublish deletes the branch, which is what makes the site 404 - dotfiles are published (.nojekyll, .well-known), .git is skipped and symlinks are skipped rather than followed - issueCLI became branchCLI with a ref field: issue commands and pages commands now share one fetch/CAS/push-with-retry path instead of two copies of it The first version read the local branch as the truth, and the local branch outlives an unpublish, so deploy said "already published" for a site that was 404ing and status called a dead site live. Both now ask the server with git ls-remote; syncSiteRef prunes a local branch the server no longer has, so a re-publish starts fresh instead of resurrecting the old history. Measured through the binary against a real instance with pages_domain set: three files deployed and served at demo.pages.test inside the one-second window, a deleted file 404ing after the next deploy, re-deploy after unpublish in 325 ms, status reporting 3 file(s) / 19 B, and unpublish twice as a no-op.
docs/roadmap.md +21
| @@ -455,6 +455,25 @@ changes bytes is broken whoever pushed it — but an admin may override, with a | |||
| 455 | 455 | tested against a writer account: moved tag, deleted tag, removed pointer, re-pointed | |
| 456 | 456 | pointer; and the audit line for the admin override. | |
| 457 | 457 | ||
| 458 | ✅ **Slice 5 — pages from a build directory.** `vole pages deploy [<dir>]` publishes | ||
| 459 | a directory as the site branch: the branch's tree becomes exactly those files, so a | ||
| 460 | page deleted from the build stops being served instead of living on until someone | ||
| 461 | notices. It is a normal commit on top of the tip (no force, no history rewrite), so a | ||
| 462 | protected `pages` branch still goes through review, and re-applying after a lost push | ||
| 463 | race is re-running the snapshot rather than merging two builds. `vole pages status` | ||
| 464 | reports the tip, the commit, the file count and the bytes; `vole pages unpublish` | ||
| 465 | deletes the branch, which is what makes the site 404. `--dry-run` prints the | ||
| 466 | added/changed/removed counts, and a deploy whose files match the published tree pushes | ||
| 467 | nothing — a build that reruns on every commit must not add an empty commit per run. | ||
| 468 | (Written as it was used: the local branch and the server's ref are two different | ||
| 469 | things. Both `status` and `deploy` ask the server with `git ls-remote` rather than | ||
| 470 | reading the clone, because a local branch outlives an unpublish — the first version | ||
| 471 | reported "already published" for a site that was 404ing, and a test caught it.) | ||
| 472 | (Measured through the binary against a real instance with `pages_domain` set: 3 files | ||
| 473 | deployed and served at `demo.pages.test` within the one-second window; a deleted file | ||
| 474 | 404d after the next deploy; re-deploy after unpublish took 325 ms; `vole pages status` | ||
| 475 | reported `3 file(s), 19 B`; unpublish twice is a no-op.) | ||
| 476 | |||
| 458 | 477 | Planned: | |
| 459 | 478 | ||
| 460 | 479 | - Identity without ceremony: `vole whoami` resolves the key a person pushes with to an | |
| @@ -629,6 +648,8 @@ Filled in as phases land. Append; don't rewrite. | |||
| 629 | 648 | | 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 | | |
| 630 | 649 | | 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 | | |
| 631 | 650 | | 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 | | |
| 651 | | 2026-10-23 | `vole pages deploy` publishes a snapshot, not a merge | A site is a build directory: merging leaves deleted pages served forever. Snapshot is a normal commit on the tip, so protection and review still apply | | ||
| 652 | | 2026-10-23 | Pages commands ask the server (`git ls-remote`), not the clone | A local branch outlives an unpublish, so the clone describes a site that 404s; the deploy said "already published" until it looked at the server | | ||
| 632 | 653 | | 2026-10-22 | A published release is immutable, admin-overridable and audited | A link whose bytes can change is worse than no link; but refusing outright leaves a mistyped tag unrepairable, so the override follows the force-push precedent: admin only, always audited | | |
| 633 | 654 | ||
| 634 | 655 | ## Open questions | |