Skip to content

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 review and publish phases do work only in image mode; gitops, governance, and docker mark them not_applicable. Capability requirements differ per mode (e.g. gitops requires Reconcile + ImpactAnalysis, plus ClusterAuth when gitops.cluster is 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 --check preview, and only execute when a human runs stagefreight ansible run <id> (--plan for preview, -e key=val for 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.