Activation¶
A deploy writes unit files and flips current — but, by default, it does not run systemctl. The new generation takes effect on next boot or a manual restart. --activate is the opt-in step that actually (re)starts the deployed units, as an effect, after the flip.
--activate¶
With --activate, after the atomic flip the deploy runs systemctl daemon-reload, then systemctl restart for each rendered unit — through the Transport (no subprocess outside the seam). It is ignored under --dry-run. Without it, the deploy is config-only.
It is opt-in, not default, so a config-only deploy (and its tests) stays unchanged, and so an operator can stage config now and activate later.
Post-flip, by theorem¶
Activation runs after the atomic flip, which covers binary + config. A failed (re)start is therefore a warning-after-success — the flip already happened and cannot be un-flipped:
deployed: stack WebStack → gen-005 (4 config file(s), 7 store path(s) pinned)
✓ (re)started appview.service (not enabled for boot)
⚠ knot.service did not (re)start: Job for knot.service failed
error: stack WebStack is deployed (current → gen-005) but 1 unit(s) did not (re)start: knot.service — investigate and re-activate; this was NOT rolled back
- A single
deployexits 1 on any activation failure (one operator action; the exit code is the signal) — but it does not roll back. Raising would imply a rollback that didn't happen. - A
fleet applyhost whose activation fails stays counted as deployed and surfaces a per-host warning — one host's post-flip restart must not abort the others.
Activation never crashes a deploy whose flip succeeded: every unit/transport failure becomes a reported result, never an exception. A daemon-reload failure reports every unit failed (nothing can start).
What gets restarted¶
Units derive from the stack's services (provenance), not from config-file paths — so a stack-declared configFiles entry that merely lives under /etc/systemd/system/ is never touched. A timer-activated service restarts its .timer only (which re-arms the schedule); restarting the bound .service would run the job now. Stateful provisioning oneshots are activated too, so the DB state is applied after the engine is up.
Boot persistence: --enable¶
--activate makes the deploy take effect now (it restarts units). --enable additionally runs systemctl enable so the units survive a reboot — the rendered units carry [Install], so this is additive. Without --enable the success message says (not enabled for boot) so the distinction is never a surprise. examples/tangled-deploy proves boot-persistence end to end (deploy once → restart the container → units cold-start).
Pluggable backends: the activator registry¶
Activation is the effect side of a backend, and it is pluggable the same way generators are. ACTIVATORS (in src/punix/deploy/activate.py) maps a backend name to an activator factory — the effect-side mirror of cli._GENERATORS:
_deploy_one selects the activator by the stack's backend post-compose. The Activator Protocol is one method — activate(units) -> tuple[UnitResult, ...]. A backend that supplies only a generator renders config fine and fails only when --activate/--enable is requested; so "render now, activate later" is a valid state, and a new supervisor (runit, launchd, …) becomes reachable by registering an activator, not by forking the core. The activator is the irreducible host-code residue — it stays core-only by design. See Extending Punix and ADR-023 for the full extensibility ladder.
Where in the code¶
src/punix/deploy/activate.py— theActivatorProtocol,SystemdActivator,units_to_activate, and theACTIVATORSregistry.
Related¶
punix service— the--activateflag.- Stateful services — why activation unblocks DB provisioning.
- Reference: decisions — ADR-017.