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 4: a published release cannot change under its link
948a6d7dcdacb72d6f90074ac0e5f9999314c4f4 · Keenan Nemetz · 17d ago · parent 85cc89fe
Phase 7 slice 4: a published release cannot change under its link The last piece of the release story is the one that makes the download links trustworthy: pre-receive now refuses to move or delete a tag that has artifacts attached, and on the metadata branch refuses removing a pointer or re-pointing one at different bytes. Comparing blob ids is enough — a pointer's id is its text, so "unchanged" needs no parsing and cannot be fooled by reformatting. Adding an artifact stays legal: an artifact is published when its pointer lands, so an addition invalidates nothing. The rules sit with the integrity checks rather than the review gate, because a download that can change bytes is broken whoever pushed it. An admin may override, with a line in data/audit.log, following the force-push precedent: a mistyped tag is otherwise unrepairable, and an unaudited override leaves no trace of the bytes that changed under somebody's link. Four refusals are tested through a writer account (moved tag, deleted tag, removed pointer, re-pointed pointer), plus the admin override and its audit line.
docs/roadmap.md +18−8
| @@ -444,6 +444,17 @@ the second deduplicating to one object and no transfer; both downloaded in 13.9 | |||
| 444 | 444 | with a matching `sha256`, `304` on `If-None-Match`, `206` on `Range: bytes=0-99`; | |
| 445 | 445 | `vole gc --dry-run` reports the published artifact reachable and removes nothing.) | |
| 446 | 446 | ||
| 447 | ✅ **Slice 4 — published releases are immutable.** `pre-receive` refuses to move or | ||
| 448 | delete a tag that `releases/<tag>/` has artifacts for, and on the metadata branch it | ||
| 449 | refuses removing a pointer or re-pointing one at different bytes (the blob id is the | ||
| 450 | pointer text, so "unchanged" needs no parsing). Additions are allowed, because an | ||
| 451 | artifact is published when its pointer lands and an addition invalidates nothing. The | ||
| 452 | rules sit with the integrity checks rather than the review gate — a download that | ||
| 453 | changes bytes is broken whoever pushed it — but an admin may override, with a line in | ||
| 454 | `data/audit.log`, because a mistyped tag is otherwise unrepairable. Four directions are | ||
| 455 | tested against a writer account: moved tag, deleted tag, removed pointer, re-pointed | ||
| 456 | pointer; and the audit line for the admin override. | ||
| 457 | |||
| 447 | 458 | Planned: | |
| 448 | 459 | ||
| 449 | 460 | - Identity without ceremony: `vole whoami` resolves the key a person pushes with to an | |
| @@ -488,14 +499,11 @@ Releases: decisions and what is left. Design settled by | |||
| 488 | 499 | ||
| 489 | 500 | Remaining: | |
| 490 | 501 | ||
| 491 | - **Slice 4 — immutability.** `pre-receive` refuses to move or delete a tag that | ||
| 492 | `releases/<tag>/` exists for, refuses a pointer path whose object id changes, and | ||
| 493 | allows adding artifacts but not removing them. Without this a download link can | ||
| 494 | silently come to mean different bytes: the ETag would change with it, which tells a | ||
| 495 | cache the truth and a human nothing. | ||
| 496 | 502 | - **Per-artifact size cap.** Artifacts ride `lfs_object_max_bytes` today, which is the | |
| 497 | 503 | same cap as an LFS object; a release of build outputs may want its own, and that is a | |
| 498 | config key to add when someone hits it. | ||
| 504 | config key to add when someone hits it, not before. | ||
| 505 | - **Signed artifacts.** Deferred with the rest of signing (see the signing block): the | ||
| 506 | only verifier available is the instance whose bytes are in question. | ||
| 499 | 507 | ||
| 500 | 508 | **Exit criteria:** a reviewer can approve a change using only the web — diff, checks and | |
| 501 | 509 | approvals on one page — without cloning; a new user can go from `vole init` to a pushed | |
| @@ -505,8 +513,9 @@ tagged release with an artifact attached over SSH downloads anonymously and matc | |||
| 505 | 513 | attached over SSH and downloaded in 13.9 ms with a matching sha256; the size in the | |
| 506 | 514 | criterion is unmeasured — the fixture was 2.9 MiB, and `lfs_object_max_bytes` is what | |
| 507 | 515 | bounds it; proven by `TestReleaseArtifactDownload` and | |
| 508 | `TestReleaseArtifactsArePrivateForPrivateRepos`). A published artifact cannot be | ||
| 509 | detached or re-pointed yet — that is slice 4, and the criterion is unmet until it is. | ||
| 516 | `TestReleaseArtifactsArePrivateForPrivateRepos`). A published artifact cannot be detached | ||
| 517 | or re-pointed ✅ (slice 4; proven by `TestPublishedReleaseIsImmutable`, which drives all | ||
| 518 | four refusals through a writer account). | ||
| 510 | 519 | ||
| 511 | 520 | ### Deferred — signing and audit | |
| 512 | 521 | ||
| @@ -620,6 +629,7 @@ Filled in as phases land. Append; don't rewrite. | |||
| 620 | 629 | | 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 | | |
| 621 | 630 | | 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 | | |
| 622 | 631 | | 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 | | |
| 632 | | 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 | | ||
| 623 | 633 | ||
| 624 | 634 | ## Open questions | |
| 625 | 635 | ||