Channels, promote, rollback
Patchline’s storage has three layers, only one of which is mutable:
objects/sha256/ab/cd/<hash> ← immutable, content-addressedreleases/<version>/manifest.json ← immutable snapshot of a versionchannels/<channel>/manifest.json ← MUTABLE pointer, rewritten by promote/rollbackThink of it as Git: objects/ is the object database, releases/<v>/manifest.json is a commit, channels/<c>/manifest.json is a branch ref.
Channels are pointers
A channel manifest is structurally identical to a release manifest. It just lives at a mutable path. When a client runs apply --channel stable, it fetches channels/stable/manifest.json, verifies the signature, and reconciles its local install against the file list.
Channels are independent of each other. beta can be on v1.2.3 while stable sits on v1.2.2 — each maintains its own monotonically-increasing release_sequence.
Publish vs. promote
| Command | What it does | Re-uploads bytes? |
|---|---|---|
publish | Scans, hashes, uploads new objects, writes a new release manifest, sets the channel pointer | Only new/changed objects |
promote | Reads an existing release manifest, points a channel at it | Never |
rollback | Same as promote — points a channel at an existing release manifest | Never |
promote and rollback are the same operation under the hood (releaseops.moveChannel). The verb is operator-facing; the storage effect is identical.
How rollback knows the old state
It doesn’t need to know — the old release manifest is still sitting at releases/<old-version>/manifest.json, and the objects it references are still in objects/sha256/.... Rollback just rewrites the channel pointer to that older manifest. The client then sees a manifest with older hashes, compares to its local files, downloads the old objects (still there), and swaps them in.
This means rollback is gated on object survival. If you ran gc after promoting v2 and v1’s unique objects had no other channel pointing at them, they’re gone. moveChannel calls verifyObjects before writing the new channel manifest and refuses the rollback rather than producing a broken pointer.
Garbage collection
patchline gc removes content-addressed objects in objects/sha256/... that aren’t referenced by any release or channel manifest. It does not touch manifests themselves.
That means:
- Release manifests are your real retention knob. As long as
releases/0.9.0/manifest.jsonexists, all of v0.9.0’s objects are pinned. - To genuinely free a version’s storage, you’d have to delete its release manifest first, then run
gc. (There’s currently no CLI command for this — the storage backend hasDeleteReleaseManifestbut it isn’t exposed.) - A common pattern: keep an
ltschannel pinned at the oldest version you want guaranteed-rollback-able to. That keeps its objects alive even after yougc.
Anti-downgrade vs. rollback
The client’s --last-sequence flag rejects manifests with release_sequence ≤ last_sequence. This protects against replay attacks (a stale manifest being served), not against operator rollback: a rollback bumps the channel’s sequence forward, so the new pointer is “newer” by sequence even though its content is older.
If you need to enforce “no client should ever go backwards in version,” that’s a different check — compare the manifest’s version field, not its sequence.
Files removed in newer versions
The client only acts on files present in the current manifest. If v2 added dlc/expansion.dat and you rollback to v1, the v1 manifest doesn’t mention that file, so it stays on the user’s disk as an orphan. Same way a fresh install of v1 into a dirty directory wouldn’t clean it. Not strictly a bug, but worth knowing if you care about leaving exactly v1’s tree on disk.