cloudfall 0.5.0__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. {cloudfall-0.5.0 → cloudfall-0.5.1}/CHANGELOG.md +21 -0
  2. {cloudfall-0.5.0 → cloudfall-0.5.1}/PKG-INFO +1 -1
  3. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/operation-decision.schema.json +6 -0
  4. {cloudfall-0.5.0 → cloudfall-0.5.1}/pyproject.toml +1 -1
  5. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/decision.py +84 -17
  6. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_decision.py +91 -6
  7. {cloudfall-0.5.0 → cloudfall-0.5.1}/uv.lock +1 -1
  8. {cloudfall-0.5.0 → cloudfall-0.5.1}/.ansible-lint +0 -0
  9. {cloudfall-0.5.0 → cloudfall-0.5.1}/.github/workflows/ci.yml +0 -0
  10. {cloudfall-0.5.0 → cloudfall-0.5.1}/.github/workflows/release.yml +0 -0
  11. {cloudfall-0.5.0 → cloudfall-0.5.1}/.gitignore +0 -0
  12. {cloudfall-0.5.0 → cloudfall-0.5.1}/ARCHITECTURE.md +0 -0
  13. {cloudfall-0.5.0 → cloudfall-0.5.1}/CONTRIBUTING.md +0 -0
  14. {cloudfall-0.5.0 → cloudfall-0.5.1}/LICENSE +0 -0
  15. {cloudfall-0.5.0 → cloudfall-0.5.1}/MANIFESTO.md +0 -0
  16. {cloudfall-0.5.0 → cloudfall-0.5.1}/README.md +0 -0
  17. {cloudfall-0.5.0 → cloudfall-0.5.1}/ROADMAP.md +0 -0
  18. {cloudfall-0.5.0 → cloudfall-0.5.1}/SECURITY.md +0 -0
  19. {cloudfall-0.5.0 → cloudfall-0.5.1}/Taskfile.yml +0 -0
  20. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/README.md +0 -0
  21. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/alert-rules/postgresql-down.yaml +0 -0
  22. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/applications/crm.yaml +0 -0
  23. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/components/crm-backend.yaml +0 -0
  24. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/domains/crm-site.yaml +0 -0
  25. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/logging-stacks/operations.yaml +0 -0
  26. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/operator-policies/production.yaml +0 -0
  27. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/server-types/debian-application.yaml +0 -0
  28. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/servers/h1.yaml +0 -0
  29. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/servers/h2.yaml +0 -0
  30. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/services/postgresql-main.yaml +0 -0
  31. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/services/redis-cache.yaml +0 -0
  32. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
  33. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/alert-rule.schema.json +0 -0
  34. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/application.schema.json +0 -0
  35. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/artifact.schema.json +0 -0
  36. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/backup-receipt.schema.json +0 -0
  37. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/common.schema.json +0 -0
  38. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/component.schema.json +0 -0
  39. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/deployment-receipt.schema.json +0 -0
  40. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/domain.schema.json +0 -0
  41. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/environment-receipt.schema.json +0 -0
  42. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/logging-stack.schema.json +0 -0
  43. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/observed-domain.schema.json +0 -0
  44. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/observed-server.schema.json +0 -0
  45. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/operation.schema.json +0 -0
  46. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/operator-policy.schema.json +0 -0
  47. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/operator-proposal.schema.json +0 -0
  48. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/release-receipt.schema.json +0 -0
  49. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/server-type.schema.json +0 -0
  50. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/server.schema.json +0 -0
  51. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/service.schema.json +0 -0
  52. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/ssh-public-key.schema.json +0 -0
  53. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
  54. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
  55. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
  56. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
  57. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
  58. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/observed/compliant/h1.json +0 -0
  59. {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/observed/compliant/h2.json +0 -0
  60. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/ansible-context.md +0 -0
  61. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/availability-design.md +0 -0
  62. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/brownfield-design.md +0 -0
  63. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
  64. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
  65. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
  66. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/fleet-goals.md +0 -0
  67. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/hybrid-storage-design.md +0 -0
  68. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/landscape.md +0 -0
  69. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/logging-service-guide.md +0 -0
  70. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/neon-migration-guide.md +0 -0
  71. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/new-server-storage-guide.md +0 -0
  72. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/operator-guide.md +0 -0
  73. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
  74. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
  75. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
  76. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
  77. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
  78. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
  79. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
  80. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
  81. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
  82. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
  83. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
  84. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/render-migration-guide.md +0 -0
  85. {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/secrets-guide.md +0 -0
  86. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/README.md +0 -0
  87. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/ansible.cfg +0 -0
  88. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/backup.yml +0 -0
  89. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/baseline.yml +0 -0
  90. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/bootstrap.yml +0 -0
  91. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/data.yml +0 -0
  92. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/deploy.yml +0 -0
  93. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/domains.yml +0 -0
  94. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/health.yml +0 -0
  95. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/inspect.yml +0 -0
  96. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/logging.yml +0 -0
  97. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/restart.yml +0 -0
  98. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/rollback.yml +0 -0
  99. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/services.yml +0 -0
  100. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/time.yml +0 -0
  101. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
  102. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
  103. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
  104. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
  105. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
  106. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
  107. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
  108. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
  109. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
  110. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
  111. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
  112. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
  113. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
  114. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
  115. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
  116. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
  117. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
  118. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
  119. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
  120. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
  121. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
  122. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
  123. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
  124. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
  125. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
  126. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
  127. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
  128. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
  129. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
  130. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
  131. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
  132. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
  133. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
  134. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
  135. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
  136. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
  137. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
  138. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
  139. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
  140. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
  141. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
  142. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
  143. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
  144. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
  145. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
  146. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
  147. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
  148. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
  149. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
  150. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
  151. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
  152. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
  153. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
  154. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
  155. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
  156. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
  157. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
  158. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
  159. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
  160. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
  161. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
  162. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
  163. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
  164. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
  165. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
  166. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
  167. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
  168. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
  169. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
  170. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
  171. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
  172. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
  173. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
  174. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
  175. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
  176. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
  177. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
  178. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
  179. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
  180. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
  181. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
  182. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
  183. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
  184. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
  185. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
  186. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
  187. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
  188. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
  189. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
  190. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
  191. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
  192. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
  193. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
  194. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/__init__.py +0 -0
  195. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/__main__.py +0 -0
  196. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
  197. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/artifact.py +0 -0
  198. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/cli.py +0 -0
  199. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/playbook.py +0 -0
  200. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/py.typed +0 -0
  201. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_ansible_inventory.py +0 -0
  202. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_artifact_builder.py +0 -0
  203. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_deploy_templates.py +0 -0
  204. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_firewall_templates.py +0 -0
  205. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_logging_templates.py +0 -0
  206. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_nginx_site_templates.py +0 -0
  207. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_playbook.py +0 -0
  208. {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_postgresql_templates.py +0 -0
  209. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/README.md +0 -0
  210. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/environment.md +0 -0
  211. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/findings.md +0 -0
  212. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/issues.md +0 -0
  213. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/linkedin.md +0 -0
  214. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/readiness.md +0 -0
  215. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-agent-dev.md +0 -0
  216. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-dev.md +0 -0
  217. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-index.md +0 -0
  218. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-issues.md +0 -0
  219. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-runtime.md +0 -0
  220. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/trace.md +0 -0
  221. {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/x.md +0 -0
  222. {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/README.md +0 -0
  223. {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/pyproject.toml +0 -0
  224. {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/render.yaml +0 -0
  225. {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/src/demo/__init__.py +0 -0
  226. {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/src/demo/asgi.py +0 -0
  227. {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/uv.lock +0 -0
  228. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/README.md +0 -0
  229. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/__init__.py +0 -0
  230. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/__main__.py +0 -0
  231. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/agent_tools.py +0 -0
  232. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/ansible_api.py +0 -0
  233. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/ansible_reader.py +0 -0
  234. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/arguments.py +0 -0
  235. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/audit.py +0 -0
  236. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/authoring.py +0 -0
  237. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/catalog.py +0 -0
  238. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/cli.py +0 -0
  239. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/commands.py +0 -0
  240. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/cutover.py +0 -0
  241. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/dashboard.py +0 -0
  242. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/dashboard_server.py +0 -0
  243. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/domain.py +0 -0
  244. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/fleet_tools.py +0 -0
  245. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/importer.py +0 -0
  246. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/inventory.py +0 -0
  247. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/lifecycle.py +0 -0
  248. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/mcp_server.py +0 -0
  249. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/migrate.py +0 -0
  250. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/observation.py +0 -0
  251. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/observe.py +0 -0
  252. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/operations.py +0 -0
  253. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/operator.py +0 -0
  254. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/project.py +0 -0
  255. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/py.typed +0 -0
  256. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/render_api.py +0 -0
  257. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/resources.py +0 -0
  258. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/secrets.py +0 -0
  259. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/service_evidence.py +0 -0
  260. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/validation.py +0 -0
  261. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_agent_tools.py +0 -0
  262. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_ansible_reader.py +0 -0
  263. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_arguments.py +0 -0
  264. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_audit.py +0 -0
  265. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_authoring.py +0 -0
  266. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_catalog.py +0 -0
  267. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_commands.py +0 -0
  268. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_cutover.py +0 -0
  269. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_dashboard_server.py +0 -0
  270. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_data_migration.py +0 -0
  271. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_fleet_tools.py +0 -0
  272. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_importer.py +0 -0
  273. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_inventory.py +0 -0
  274. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_lifecycle.py +0 -0
  275. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_migrate.py +0 -0
  276. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_observe.py +0 -0
  277. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_operations_dashboard.py +0 -0
  278. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_operator.py +0 -0
  279. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_project.py +0 -0
  280. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_render_api.py +0 -0
  281. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_resources.py +0 -0
  282. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_secrets.py +0 -0
  283. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_service_evidence.py +0 -0
  284. {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_validation.py +0 -0
  285. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/apple-touch-icon.png +0 -0
  286. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/favicon-32.png +0 -0
  287. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/favicon.svg +0 -0
  288. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/icon-192.png +0 -0
  289. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/icon-512.png +0 -0
  290. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/index.html +0 -0
  291. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/manifesto.html +0 -0
  292. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/og.png +0 -0
  293. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/site.webmanifest +0 -0
  294. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/src/worker.js +0 -0
  295. {cloudfall-0.5.0 → cloudfall-0.5.1}/site/wrangler.jsonc +0 -0
@@ -6,6 +6,27 @@ Notable changes to Cloudfall. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.1] — 2026-09-22
10
+
11
+ ### Changed
12
+
13
+ - Decision records default to `decisions/` beside the operations rather
14
+ than to `tmp/decisions`: the record is what a team keeps, and `tmp/` is
15
+ what they throw away. The diffs and logs beside each record are raw
16
+ Ansible output, so a repository that commits them wants `no_log` on the
17
+ tasks that handle secrets
18
+
19
+ ### Fixed
20
+
21
+ - A decision is `verified` only when its verify run changed nothing. A
22
+ verify step that exits zero while changing a host found the fleet not as
23
+ the run left it and converged it further, which verifies nothing, so it
24
+ is recorded `failed`. Every approved decision now carries a `verdict`
25
+ saying why it ended where it did
26
+ - A record cites its artifacts and its observation basis as the repository
27
+ sees them rather than by absolute path, because a record is committed
28
+ and one machine's home directory means nothing in anyone else's checkout
29
+
9
30
  ## [0.5.0] — 2026-09-21
10
31
 
11
32
  ## [0.4.0] — 2026-09-21
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cloudfall
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: AI-native control plane for dedicated Debian application fleets
5
5
  Project-URL: Homepage, https://cloudfall.dev
6
6
  Project-URL: Repository, https://github.com/romamo/cloudfall
@@ -337,6 +337,12 @@
337
337
  ],
338
338
  "additionalProperties": false,
339
339
  "description": "What the operation's verify playbook reported afterwards."
340
+ },
341
+ "verdict": {
342
+ "type": "string",
343
+ "description": "Why the decision ended in its status.",
344
+ "minLength": 1,
345
+ "maxLength": 300
340
346
  }
341
347
  },
342
348
  "required": [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cloudfall"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "AI-native control plane for dedicated Debian application fleets"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -42,8 +42,15 @@ type CheckRunner = Callable[[Sequence[str], Mapping[str, str], Path], int]
42
42
  API_VERSION = "cloudfall/v1"
43
43
  DECISION_KIND = "OperationDecision"
44
44
  DECISION_SCHEMA = "operation-decision.schema.json"
45
- DECISION_DIRECTORY = "tmp/decisions"
46
- """Where decision records and their diffs land inside the repository."""
45
+ DECISION_DIRECTORY = "decisions"
46
+ """Where decision records and their artifacts land inside the repository.
47
+
48
+ Beside the operations rather than under ``tmp/``: the record is what the
49
+ team keeps, and the runtime directory is what they throw away. Commit the
50
+ records; the diffs and logs beside them are raw Ansible output, so they
51
+ belong in git only in a repository whose tasks set ``no_log`` where it
52
+ matters.
53
+ """
47
54
 
48
55
  _TREE_DIRECTORY = "tree"
49
56
  _DIFF_SUFFIX = ".diff"
@@ -248,6 +255,7 @@ class Decision:
248
255
  approval: Approval | None = None
249
256
  execution: RunRecord | None = None
250
257
  verification: RunRecord | None = None
258
+ verdict: str | None = None
251
259
 
252
260
  def as_document(self) -> dict[str, object]:
253
261
  """Serialize the decision as a schema-valid record document."""
@@ -274,6 +282,8 @@ class Decision:
274
282
  spec["execution"] = self.execution.as_dict()
275
283
  if self.verification is not None:
276
284
  spec["verify"] = self.verification.as_dict()
285
+ if self.verdict is not None:
286
+ spec["verdict"] = self.verdict
277
287
  return {
278
288
  "apiVersion": API_VERSION,
279
289
  "kind": DECISION_KIND,
@@ -399,8 +409,8 @@ def propose(
399
409
  inputs=declared,
400
410
  requirement=requirement,
401
411
  reason=reason,
402
- basis=read_basis(request.observations),
403
- check=_preview(exit_code, tree, diff_path),
412
+ basis=read_basis(request.observations, request.repository),
413
+ check=_preview(exit_code, tree, diff_path, request.repository),
404
414
  status=DecisionStatus.PROPOSED,
405
415
  )
406
416
  store.save(decision)
@@ -468,6 +478,7 @@ def approve(
468
478
  if decision.verify_playbook is not None and execution.exit_code == 0
469
479
  else None
470
480
  )
481
+ status, verdict = _outcome(execution, verification, decision.verify_playbook)
471
482
  approved = replace(
472
483
  decision,
473
484
  approval=Approval(
@@ -475,7 +486,8 @@ def approve(
475
486
  ),
476
487
  execution=execution,
477
488
  verification=verification,
478
- status=_outcome(execution, verification, decision.verify_playbook),
489
+ status=status,
490
+ verdict=verdict,
479
491
  )
480
492
  store.update(approved)
481
493
  return approved
@@ -485,15 +497,42 @@ def _outcome(
485
497
  execution: RunRecord,
486
498
  verification: RunRecord | None,
487
499
  verify_playbook: Path | None,
488
- ) -> DecisionStatus:
489
- """Return the status one run earned: unverified is not the same as done."""
500
+ ) -> tuple[DecisionStatus, str]:
501
+ """Return the status one run earned, and why, for the record.
502
+
503
+ Verification is a claim about the fleet's state, so a verify run that
504
+ changed something did not verify anything: it found the fleet not as
505
+ the operation left it and converged it further. Exiting zero is not
506
+ enough.
507
+ """
490
508
  if execution.exit_code != 0:
491
- return DecisionStatus.FAILED
509
+ return (
510
+ DecisionStatus.FAILED,
511
+ f"the run exited {execution.exit_code}",
512
+ )
492
513
  if verify_playbook is None:
493
- return DecisionStatus.EXECUTED
494
- if verification is None or verification.exit_code != 0:
495
- return DecisionStatus.FAILED
496
- return DecisionStatus.VERIFIED
514
+ return (
515
+ DecisionStatus.EXECUTED,
516
+ "the run succeeded; the operation declares no verify step",
517
+ )
518
+ if verification is None: # pragma: no cover - callers verify after a clean run
519
+ return (DecisionStatus.FAILED, "the verify step did not run")
520
+ if verification.exit_code != 0:
521
+ return (
522
+ DecisionStatus.FAILED,
523
+ f"the verify step exited {verification.exit_code}",
524
+ )
525
+ if verification.changed:
526
+ changed = ", ".join(verification.changed)
527
+ return (
528
+ DecisionStatus.FAILED,
529
+ "the verify step changed "
530
+ f"{changed}, so the fleet was not in the state the run claimed",
531
+ )
532
+ return (
533
+ DecisionStatus.VERIFIED,
534
+ "the run succeeded and the verify step changed nothing",
535
+ )
497
536
 
498
537
 
499
538
  def _run_stage(
@@ -513,7 +552,9 @@ def _run_stage(
513
552
  argv, environment = run_command(invocation)
514
553
  log_path.parent.mkdir(parents=True, exist_ok=True)
515
554
  exit_code = stage.runner(argv, environment, log_path)
516
- preview = _preview(exit_code, invocation.tree, log_path)
555
+ preview = _preview(
556
+ exit_code, invocation.tree, log_path, stage.request.repository
557
+ )
517
558
  return RunRecord(
518
559
  ran_at=_timestamp(moment),
519
560
  exit_code=exit_code,
@@ -554,7 +595,19 @@ def run_command(
554
595
  return tuple(argv), environment
555
596
 
556
597
 
557
- def read_basis(observations: Path) -> Basis:
598
+ def repository_relative(path: Path, repository: Path) -> Path:
599
+ """Return the path as the repository sees it, when it lies inside it.
600
+
601
+ Records are committed, so a path that names one machine's home
602
+ directory is worse than useless to everyone else reading the trail.
603
+ """
604
+ try:
605
+ return path.resolve().relative_to(repository.resolve())
606
+ except ValueError:
607
+ return path
608
+
609
+
610
+ def read_basis(observations: Path, repository: Path | None = None) -> Basis:
558
611
  """Return the observation evidence a proposal cites.
559
612
 
560
613
  A proposal made against no evidence says so, rather than implying it
@@ -571,10 +624,19 @@ def read_basis(observations: Path) -> Basis:
571
624
  spec = document.get("spec")
572
625
  if isinstance(spec, dict) and isinstance(spec.get("observedAt"), str):
573
626
  observed_at[path.stem] = str(spec["observedAt"])
574
- return Basis(observations=observations, observed_at=observed_at)
627
+ return Basis(
628
+ observations=(
629
+ repository_relative(observations, repository)
630
+ if repository is not None
631
+ else observations
632
+ ),
633
+ observed_at=observed_at,
634
+ )
575
635
 
576
636
 
577
- def _preview(exit_code: int, tree: Path, diff_path: Path) -> CheckPreview:
637
+ def _preview(
638
+ exit_code: int, tree: Path, diff_path: Path, repository: Path | None = None
639
+ ) -> CheckPreview:
578
640
  changed: list[str] = []
579
641
  unchanged: list[str] = []
580
642
  if tree.is_dir():
@@ -596,7 +658,11 @@ def _preview(exit_code: int, tree: Path, diff_path: Path) -> CheckPreview:
596
658
  changed=tuple(changed),
597
659
  unchanged=tuple(unchanged),
598
660
  diff=DiffArtifact(
599
- path=diff_path,
661
+ path=(
662
+ repository_relative(diff_path, repository)
663
+ if repository is not None
664
+ else diff_path
665
+ ),
600
666
  sha256=hashlib.sha256(content).hexdigest(),
601
667
  size=len(content),
602
668
  ),
@@ -768,6 +834,7 @@ def _decision_from_document(document: Mapping[str, object]) -> Decision:
768
834
  ),
769
835
  ),
770
836
  status=DecisionStatus(str(spec["status"])),
837
+ verdict=str(spec["verdict"]) if "verdict" in spec else None,
771
838
  execution=_run_from_document(spec.get("execution")),
772
839
  verification=_run_from_document(spec.get("verify")),
773
840
  approval=(
@@ -11,6 +11,7 @@ import pytest
11
11
  from cloudfall.catalog import RiskLevel, TargetScope, load_catalog
12
12
  from cloudfall.cli import main
13
13
  from cloudfall.decision import (
14
+ DECISION_DIRECTORY,
14
15
  ApprovalRequest,
15
16
  DecisionError,
16
17
  DecisionStatus,
@@ -49,7 +50,7 @@ verify:
49
50
 
50
51
  def _store(repository: Path) -> DecisionStore:
51
52
  return DecisionStore(
52
- directory=repository / "tmp" / "decisions",
53
+ directory=repository / DECISION_DIRECTORY,
53
54
  catalog=SchemaCatalog(SCHEMAS),
54
55
  )
55
56
 
@@ -84,13 +85,21 @@ def _proposal(
84
85
 
85
86
 
86
87
  class _Runner:
87
- """A check runner that writes what Ansible would write."""
88
+ """A runner that writes what Ansible would write, stage by stage.
89
+
90
+ An idempotent fleet changes on the run and changes nothing on the
91
+ verify that follows, so the two stages are modelled separately.
92
+ """
88
93
 
89
94
  def __init__(
90
- self, exit_code: int = 0, changed: tuple[str, ...] = ("web-1",)
95
+ self,
96
+ exit_code: int = 0,
97
+ changed: tuple[str, ...] = ("web-1",),
98
+ verify_changed: tuple[str, ...] = (),
91
99
  ) -> None:
92
100
  self.exit_code = exit_code
93
101
  self.changed = changed
102
+ self.verify_changed = verify_changed
94
103
  self.recorded: list[tuple[str, ...]] = []
95
104
 
96
105
  def __call__(
@@ -99,15 +108,20 @@ class _Runner:
99
108
  environment: Mapping[str, str],
100
109
  diff_path: Path,
101
110
  ) -> int:
102
- self.recorded.append(tuple(argv))
103
111
  tree = Path(environment["ANSIBLE_CALLBACK_TREE_DIR"])
112
+ changed = self.verify_changed if self._verifying(tree) else self.changed
113
+ self.recorded.append(tuple(argv))
104
114
  tree.mkdir(parents=True, exist_ok=True)
105
- for host in self.changed:
115
+ for host in changed:
106
116
  (tree / host).write_text('{"changed": true}', encoding="utf-8")
107
117
  (tree / "web-2").write_text('{"changed": false}', encoding="utf-8")
108
118
  diff_path.write_text("--- before\n+++ after\n", encoding="utf-8")
109
119
  return self.exit_code
110
120
 
121
+ @staticmethod
122
+ def _verifying(tree: Path) -> bool:
123
+ return tree.name.endswith("-verify")
124
+
111
125
 
112
126
  def test_the_gate_follows_the_risk_level() -> None:
113
127
  """The table is the product's: a human cannot be skipped by policy."""
@@ -131,7 +145,11 @@ def test_a_proposal_records_what_check_mode_would_change(tmp_path: Path) -> None
131
145
  assert decision.check.changed == ("web-1",)
132
146
  assert decision.check.unchanged == ("web-2",)
133
147
  assert decision.check.diff.size == len(b"--- before\n+++ after\n")
134
- assert decision.check.diff.path.read_text(encoding="utf-8").startswith("---")
148
+ # A committed record names paths as the repository sees them.
149
+ assert not decision.check.diff.path.is_absolute()
150
+ artifact = repository / decision.check.diff.path
151
+ assert artifact.read_text(encoding="utf-8").startswith("---")
152
+ assert not decision.basis.observations.is_absolute()
135
153
 
136
154
 
137
155
  def test_a_proposal_cites_the_evidence_it_was_made_against(
@@ -442,6 +460,10 @@ def test_an_approval_runs_the_recorded_proposal_and_verifies_it(
442
460
  assert approved.execution is not None
443
461
  assert approved.execution.changed == ("web-1",)
444
462
  assert approved.verification is not None
463
+ assert approved.verification.changed == ()
464
+ assert approved.verdict == (
465
+ "the run succeeded and the verify step changed nothing"
466
+ )
445
467
  assert [argv[1] for argv in run.recorded] == ["--diff", "--diff"]
446
468
  assert run.recorded[0][-1].endswith("playbooks/deploy.yml")
447
469
  assert run.recorded[1][-1].endswith("playbooks/health.yml")
@@ -630,3 +652,66 @@ def test_an_approved_decision_round_trips_through_the_store(
630
652
  assert loaded.execution.exit_code == 0
631
653
  assert loaded.verification is not None
632
654
  assert loaded.status is DecisionStatus.VERIFIED
655
+
656
+
657
+ def test_a_verify_step_that_changes_the_fleet_did_not_verify_it(
658
+ tmp_path: Path,
659
+ ) -> None:
660
+ """Exiting zero is not enough: verification is a claim about state."""
661
+ repository = _repository(tmp_path)
662
+ store = _store(repository)
663
+ proposed = propose(_proposal(repository), store, lambda: MOMENT, _Runner())
664
+
665
+ approved = approve(
666
+ ApprovalRequest(
667
+ decision=proposed, approver="roman", repository=repository
668
+ ),
669
+ store,
670
+ lambda: MOMENT,
671
+ _Runner(verify_changed=("web-1",)),
672
+ )
673
+
674
+ assert approved.status is DecisionStatus.FAILED
675
+ assert approved.verification is not None
676
+ assert approved.verification.exit_code == 0
677
+ assert approved.verification.changed == ("web-1",)
678
+ assert approved.verdict is not None
679
+ assert "the verify step changed web-1" in approved.verdict
680
+
681
+
682
+ def test_every_outcome_says_why_in_the_record(tmp_path: Path) -> None:
683
+ repository = _repository(tmp_path, deploy=DEPLOY, facts=FACTS)
684
+ store = _store(repository)
685
+
686
+ failed = approve(
687
+ ApprovalRequest(
688
+ decision=propose(
689
+ _proposal(repository), store, lambda: MOMENT, _Runner()
690
+ ),
691
+ approver="roman",
692
+ repository=repository,
693
+ ),
694
+ store,
695
+ lambda: MOMENT,
696
+ _Runner(exit_code=2),
697
+ )
698
+ executed = approve(
699
+ ApprovalRequest(
700
+ decision=propose(
701
+ _proposal(repository, "facts", target=None, inputs={}),
702
+ store,
703
+ lambda: MOMENT,
704
+ _Runner(),
705
+ ),
706
+ approver="roman",
707
+ repository=repository,
708
+ ),
709
+ store,
710
+ lambda: MOMENT,
711
+ _Runner(),
712
+ )
713
+
714
+ assert failed.verdict == "the run exited 2"
715
+ assert executed.verdict == (
716
+ "the run succeeded; the operation declares no verify step"
717
+ )
@@ -254,7 +254,7 @@ wheels = [
254
254
 
255
255
  [[package]]
256
256
  name = "cloudfall"
257
- version = "0.5.0"
257
+ version = "0.5.1"
258
258
  source = { editable = "." }
259
259
  dependencies = [
260
260
  { name = "ansible-core" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes