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/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