Table of contents
- ADR-005: Treat automatic updates as a fleet deployment control plane
- Context
- Decision
- Consequences
- Alternatives considered
- Let each host run nix flake update
- Have one timer update and push flake.lock without closure builds
- Build first, then rebase and push
- Treat partial group failure as total failure or total success
- Let the remediation model repair and merge automatically
- Remove emergency or manual controls
- References
ADR-005: Treat automatic updates as a fleet deployment control plane
- Status: Accepted
- Date: 2026-07-17
- Scope: Flake-input updates, host closure validation, cache and Git publication, consumer activation, recovery, and remediation
- Supersedes: None
- Superseded by: None
Context
An unattended update can change privileged configuration on every managed host. The original timer-shaped implementation grew in response to concrete failures and review findings: omitted inputs or hosts, wrong-target builds, activation outliving its parent service, races with maintenance holds, hidden partial failures, ambiguous deployed revisions, and a producer validating one Git base before publishing against another.
These are distributed deployment problems, not merely package-refresh
problems. A successful nix flake update proves only that a lock file was
written; even nix flake check does not prove that every affected host closure
was built, cached, published from the validated tree, and activated without
racing another state transition.
The July 2026 reliability review established fail-closed input classification,
serialized detached activation, explicit partial outcomes, exact lease-aware
publication, fast-forward-only consumption, and required regression/KVM state
machine tests. The implementation was later extracted into the standalone
nixos-auto-update package/module flake while this repository retained fleet
policy, producer workflow integration, and remediation controls.
Decision
Automatic updates are a small fleet deployment control plane with distinct producer, publication, consumer, and advisory-remediation stages.
Producer
The normal producer is the Forgejo Actions workflow running on Nova's Fedora runner. It must:
- derive the complete root-input inventory and reject any input that is neither classified into a host-impact group nor explicitly pinned;
- update groups in isolation so one incompatible candidate does not hide or unnecessarily reject independent successful groups;
- run updater regression tests and the NixOS state-machine check before expensive closure publication;
- build every host closure required by the accepted input groups;
- push those exact closures to Attic; and
- publish a traceable
flake.lockcommit only when the validated base revision, lock hash, and closure manifest still match.
Publication uses an exact atomic Git lease. The producer must not rebase a
validated result onto a moving main, because the combined tree was never the
tree that passed validation.
Consumers
Managed NixOS hosts are pull-only consumers. A consumer must fast-forward to a published revision, build and activate that exact revision, preserve the active Electra specialisation where applicable, and record a result that can be correlated with repository provenance.
Update, activation, and maintenance transitions share serialized state. A detached activation helper may outlive the initiating systemd process, so locks, progress markers, holds, and cleanup must cover the helper's real lifetime. Missing or ambiguous state fails closed and remains visible through status and notifications.
Freshness windows, randomized schedules, manual force controls, deployment holds, and an emergency producer path are part of this control plane. They must obey the same provenance and serialization invariants rather than bypassing them for convenience.
Remediation
Rejected input updates produce bounded evidence. A guarded Nova worker may diagnose the failure and prepare a narrowly validated temporary-pin proposal, but model output is advisory. It cannot auto-merge, alter the validated evidence after review, or receive unrelated notification credentials. Pin addition and removal remain review-only changes with explicit lifecycle tracking.
Core invariants
- Every root input is classified or deliberately pinned.
- Every accepted lock change has all required consumer closures built.
- The validated base, lock hash, closure manifest, cached closures, and published commit refer to the same artefacts.
- Publication is exact and atomic; loss of the lease fails closed.
- Consumers fast-forward and activate the exact published revision.
- Only one update, activation, hold, or maintenance transition owns the state machine at a time.
- Missing, partial, or contradictory state is reported rather than collapsed into success.
- Deterministic tests gate builds, cache publication, and Git publication.
Consequences
- Hosts consume one reviewed and cache-warmed fleet result instead of producing divergent lock files independently.
- A package candidate can be rejected for only its affected group while independent groups continue, with the partial outcome remaining visible.
- Publication and activation are slower and more complex than a simple timer, but every dangerous transition has provenance and a recovery boundary.
- Attic is an optimisation and a publication artefact store; consumers still verify and build the exact system closure rather than trusting a notification alone.
- The system requires shell regression coverage, a real systemd/Git state machine test, workflow routing tests, and live deployment/no-op evidence for material changes.
- External availability of Forgejo, the runner, Attic, Git remotes, and notifications remains a failure domain. The pipeline can fail safely and visibly; it cannot guarantee those services.
- Closure success does not prove every peripheral or application workflow on physical hardware. Risky changes still require the canary and deployment evidence defined by project governance.
- Nova's staged NixOS producer configuration and the live Fedora Forgejo runner are intentionally not conflated. A future Nova deployment must explicitly hand over producer ownership.
- The implementation is large enough to justify its standalone package/module boundary; extraction must preserve the invariants rather than simplifying away safety properties.
Alternatives considered
Let each host run nix flake update
Rejected. Hosts could select different dependency sets, repeat expensive builds, publish no shared proof, and fail independently after an upstream update.
Have one timer update and push flake.lock without closure builds
Rejected. Evaluation or lock generation is not deployment validation. It would publish candidates before proving that every affected system can build.
Build first, then rebase and push
Rejected. Rebasing creates an unvalidated tree from a validated lock change and a newer base. Exact lease failure must cause a fresh producer run.
Treat partial group failure as total failure or total success
Rejected. Total rejection delays independent safe updates; total success hides the incompatible candidate. Per-group outcomes preserve both progress and diagnostic truth.
Let the remediation model repair and merge automatically
Rejected. A model may propose a bounded pin after deterministic reproduction, but it does not hold merge or deployment authority. Human review remains the boundary for changes that intentionally diverge from upstream updates.
Remove emergency or manual controls
Rejected. Recovery needs a path when the normal producer is unavailable, and operators need holds and forced checks. Those controls are safe only when they participate in the same locking, provenance, and publication rules.
References
- Current topology and boundaries: Automation and Control Plane
- Incident procedures: Auto-Update Incident Response
- Change and rollout requirements: Project and Change Governance
- Current source:
lib/auto-update-policy.nix,.forgejo/workflows/attic-cache.yml,scripts/attic-cache-build.sh,scripts/nixos_update_remediation/,tests/nixos-update-remediation-regressions.sh, and the pinnednixos-auto-updateflake input and its exported state-machine check - Reliability review evidence: Forgejo issues #51, #52, #53, #55, and #56
- Input classification: repository commit
a3fb725fbe54f333fd159a1ee2b782c9381911ec - Lease-aware publication: repository commit
9eb45a94e63f7f98437be0f02f0a10e0cb9b0fdf - State-machine tests and required workflow gate: repository commits
769e8c36edc79401afc6717d626d25624d88f91cand19363f83a8740b7bcfa7ff350513060fa48049ed - Standalone module adoption: repository commits
c70b92fb4455fac61a0d069c5f555a1d9d3a34a4and569d02ef9fc31db63c77194f0c249cd602a7057d - Guarded remediation worker: repository commit
c25788f9e6b94d9d8da821ae95285993429da34c