Lifecycle & Convergence¶
The single most architecturally significant choice: lifecycle.mode selects the phase graph
the pipeline runs — build container images (image, the default), validate GitOps manifests
(gitops), reconcile governance (governance), or drive the docker lifecycle (docker).
Each mode has its own config section.
Convergence subsystems COEXIST rather than compete: the perform phase runs every configured
reconciler — a gitops repo keeps mode: gitops and adds ansible: beside it, and both
converge in the same reconcile pass. Future host/infra backends join this page the same way.
Real examples
dungeon runs mode: gitops; MaintenancePolicy runs mode: governance — see
Getting Started.
lifecycle¶
Selects the repository lifecycle mode — the phase graph the pipeline runs. The single most architecturally significant config choice: it determines whether the repo builds container images, validates GitOps manifests, or reconciles governance. When omitted, the lifecycle defaults to image.
lifecycle:
preset: <string> # Preset references an external lifecycle fragment to inherit (the generic preset: fragment-include…
mode: <string> # Mode selects the phase graph. Empty defaults to image. image — build → review → publish image… · required
Phase applicability is mode-derived: the
reviewandpublishphases do work only inimagemode;gitops,governance, anddockermark them not_applicable. Capability requirements differ per mode (e.g.gitopsrequires Reconcile + ImpactAnalysis, plus ClusterAuth whengitops.clusteris set); the lifecycle backend is checked against them at plan time.
gitops¶
GitOps defines configuration for the gitops lifecycle mode.
gitops:
preset: <string>
backend: <string> # Backend selects the GitOps reconciliation backend (e.g. "flux", "argo"). · required
cluster: # Cluster defines the target Kubernetes cluster. · required
name: <string> # required
server: <string> # required
exposure: # required
rules: # required
- level: <string> # internet | intranet | cluster · required
endpoints: [<string>] # ip:port (highest precedence) · required
gateways: [<string>] # required
cidrs: [<string>] # required
ports: [...] # AND with CIDRs (empty = any port) · required
service_types: [<string>] # ClusterIP | NodePort | LoadBalancer · required
oidc: # OIDC defines authentication configuration for the cluster. · required
audience: <string> # required
governance¶
Governance defines configuration for the governance lifecycle mode. Only valid in the control repo (lifecycle.mode: governance).
governance:
clusters: # required
- id: <string> # required
targets: # required
repos: [<string>]
groups:
- id: <string>
repos: [<string>] # required
credentials: <string> # env var prefix for forge auth
stagefreight: {} # required
docker¶
Docker defines configuration for the docker lifecycle mode.
docker:
preset: <string>
backend: <string> # Backend selects the Docker execution engine (e.g. "compose"). · required
targets: # Targets defines how reconciliation targets are resolved. · required
source: <string> # Source is the inventory adapter (e.g. "ansible"). · required
inventory: <string> # Inventory is the path to the inventory file (relative to repo root). · required
selector: # Selector declares which hosts from inventory are eligible. · required
groups: [<string>] # required
iac: # IaC defines the Infrastructure as Code directory layout. · required
path: <string> # Path is the IaC directory relative to repo root (default: "docker-compose"). · required
secrets: # Secrets defines the secrets decryption provider. · required
provider: <string> # Provider selects the secrets backend (e.g. "sops", "vault", "infisical"). · required
drift: # Drift defines drift detection and reconciliation policy. · required
tier2_action: <string> # report | reconcile (default: report) · required
orphan_action: <string> # report | down | prune (default: report) · required
orphan_threshold: <int> # block if more than N orphans (default: 5) · required
prune_requires_confirmation: false # require --force for prune (default: true) · required
ansible¶
Ansible defines the ansible host-convergence subsystem. Presence-gated (any converge playbook activates it) and independent of lifecycle.mode.
ansible:
preset: <string>
backend: <string> # Backend selects the host-convergence backend. Default: "ansible". · required
image: <string> # Image is the execution image the playbooks (and ansible-lint) run in — the ansible runtime… · required
inventory: <string> # Inventory is the repo-relative ansible inventory file. · required
ssh: # SSH is the shared connection identity used by every play. · required
user: <string> # User is the remote login user on the managed hosts. · required
credentials: <string> # Credentials is the env-prefix name the SSH key material is read from. · required
known_hosts: <string> # KnownHosts is the repo-relative known_hosts file holding the managed hosts' public keys. Host-key… · required
playbooks: # Playbooks is the play library: an order-preserving id → entry map. Entries with converge: true… · required
<id>: # entry key = the unique id
path: <string> # Path is the repo-relative playbook file. · required
groups: [<string>] # Groups are the inventory groups this play targets (rendered as --limit). · required
converge: false # Converge marks the play as desired-state: it runs on every perform reconcile. False declares a… · required
required: false # Required: failure is hard pipeline fail (default: true — a converge mutates hosts, so a partial…
Ansible host convergence¶
The ansible: subsystem converges hosts the way gitops converges clusters: a declared
playbook LIBRARY, executed inside a pinned execution image, on every perform reconcile. Two
lanes, one backend:
- Converge plays (
converge: true) run in CI, in declared order, fail-loud — any failed or unreachable host fails the phase; no silent partial converge. Idempotency lives in the playbook (per-host "already at the pin → skip"), so every run is safe and steady-state is a fast no-op. - Runbooks (
converge: false) are declared but structurally unreachable from CI — they get lint, the pinned image, and--checkpreview, and only execute when a human runsstagefreight ansible run <id>(--planfor preview,-e key=valfor launch-time vars).
Execution image. Playbooks and ansible-lint run inside ansible.image — the image owns
the ansible runtime, collections, and connection deps (pywinrm for Windows nodes). Official
default: docker.io/hlhd/ansible; bring your own by
pointing image: elsewhere. Pin a version tag — updates ride the deps engine's docker-image
tag-line semantics.
Trust posture. The SSH key resolves from <PREFIX>_SSH_KEY (raw PEM) or
<PREFIX>_SSH_KEY_B64 (base64 — the single-line form a masked variable can hold, mirroring
<NAME>_CA_B64); store it masked + protected so unprotected-ref pipelines never receive
it — they render "skipped — credentials not available", the signature of a correctly-protected
setup. Host-key verification is always strict against the repo-committed ssh.known_hosts
(ssh-keyscan the fleet once); host trust is auditable in git. The perform job carries a
serialization group (resource_group on GitLab) so two pipelines queue rather than race a
cordon/drain.
Audition lint. lint.modules.ansible runs ansible-lint from the same execution image over
exactly the declared plays — undeclared ansible files stay quiet until they join the library.
Severities are graduated (blocker/critical → warning, rest informational): adopting the module
never hard-fails an audition on style rules.
Facts. Converge results record as the ansible subsystem: {ansible.converged},
{ansible.total}, {ansible.changed}, {ansible.unreachable} — and the shipped summary
carries Converged {ansible.converged}/{ansible.total} nodes · {ansible.changed} changed,
eliding on runs without ansible, exactly like the gitops reconcile line. Failures narrate via
the standard failure facts with no extra wiring.
Drift cadence. StageFreight converges when pipelines run; add a forge scheduled pipeline (e.g. weekly) as drift insurance — steady-state converge is a fast no-op.
Testing. Molecule needs no dedicated machinery — run it as a tool: script test suite
from the execution image. See examples/ansible.yml for the complete
annotated example.