Skip to content

Rollback

punix service rollback STACK flips the live current symlink to gen-(current-1). O(1) — no rebuild, no re-evaluation, no fetch. The conceptual model is in Generations and rollback; this page is the operator's how-to.

The minimal recipe

punix service rollback MyStack

That's it. Output:

rolled back: stack MyStack → gen-001 (was gen-002)

Two gen-NNN.json files exist on disk afterwards; current points at the older one. Rollback is "flip the pointer", not "delete forward generations" — you can roll forward by re-deploying.

What it does

1. Read manifest: current = N
2. If N is None or N <= 1: [E12] "no previous generation"
3. prev = N - 1
4. verify_pinned_paths(stack, prev, store)
       └─ for each path in gen-(prev).store_paths:
            require Store.is_valid(path)
       └─ any missing path: [E12] with the full list
5. atomic_symlink_switch(gen-(prev).json, current)   # the atomic flip

The verify step catches GC contamination: if a path the previous gen needs has been deleted, rollback refuses before flipping — so you don't end up with current pointing at a generation whose binaries are gone.

When rollback fails

[E12] no previous generation

error: [E12] stack 'MyStack': no previous generation (current = 1);
nothing to roll back to.

current points at gen-001.json — there's nothing earlier. Two options:

  • Re-deploy from a known-good PCL version.
  • Wait for Stage 8 — a planned service set-current STACK N command will allow flipping to any preserved gen, not just cur-1.

[E12] pinned store path missing

error: [E12] stack 'MyStack' gen-001: pinned store path missing:
  /store/abc-curl-8.5.0
  /store/def-openssl-3.0.0
This usually means GC ran with the wrong root set. Recover by
re-realising: punix build pkgs/

GC walked your store with a keep-set that didn't include this generation. Most likely cause: someone rm -rf'd the store, or you GC'd a deployments-root that wasn't the live one. Recover:

  1. Re-realise the closure: punix build pkgs/ --store-root <same as deploy used>. The store paths are content-addressed; rebuilding produces the same paths (so the manifest's pin still matches).
  2. Re-try rollback: punix service rollback MyStack.

If you can't rebuild (PCL source is gone), the previous gen is unrecoverable. This is why generations and the store live in different directories — losing one doesn't lose the other.

Multi-step rollback

v0.6 ships one-step rollback only. To roll back two steps:

punix service rollback MyStack   # gen-003 → gen-002
punix service rollback MyStack   # gen-002 → gen-001

The intermediate flip is fully atomic; the combined operation is not (another deploy could land between them). Stage 8's planned service set-current STACK N makes multi-step atomic.

Verifying after rollback

# What gen is current?
cat <deployments_root>/MyStack/current      # → gen-001.json
# (it's a symlink — readlink shows the target name)

# What config files does that gen want on disk?
cat <deployments_root>/MyStack/gen-001.json | jq '.config_files[]'

# Spot-check a sha256
sha256sum /etc/myapp/app.conf
# compare with gen-001.json's config_files[0].sha256

If the disk-file sha256 doesn't match the gen's, someone mutated /etc/myapp/app.conf out-of-band since the deploy. Rollback does not re-write config files in v0.6 (it just flips current; config files were already written at the deploy that produced that gen). The disk-file drift is a known small gap — Stage 8 will add an opt-in "rollback also re-writes config files."

Rollback over SSH

Same command shape with --target:

punix service rollback MyStack \
  --target ssh://deploy@prod.example.com \
  --known-hosts ./ci/prod-known_hosts

The atomic flip runs on the remote via mv (which invokes rename(2)). Same atomicity guarantee as the local case. The verify_pinned_paths step uses the LOCAL store on the deploy host — known cross-host gap recorded in status: limitations.

Conformance

  • Rollback restores the previous gen exactly.
  • Rollback after a normal GC pass succeeds (the GC kept the previous gen's pins).
  • A missing pinned path surfaces a located [E12] error before current is touched.

All three live in tests/c_e2e/test_conformance_stage4.py (and an SSH variant for .1/.2 in test_conformance_stage5_ssh.py).