cloudfall 0.2.0__tar.gz → 0.3.0__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 (282) hide show
  1. {cloudfall-0.2.0 → cloudfall-0.3.0}/CHANGELOG.md +43 -0
  2. {cloudfall-0.2.0 → cloudfall-0.3.0}/PKG-INFO +24 -14
  3. {cloudfall-0.2.0 → cloudfall-0.3.0}/README.md +23 -13
  4. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/render-migration-guide.md +1 -1
  5. {cloudfall-0.2.0 → cloudfall-0.3.0}/pyproject.toml +1 -1
  6. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/cli.py +2 -25
  7. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/project.py +48 -163
  8. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_agent_tools.py +8 -3
  9. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_authoring.py +8 -3
  10. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_project.py +57 -131
  11. {cloudfall-0.2.0 → cloudfall-0.3.0}/uv.lock +1 -1
  12. {cloudfall-0.2.0 → cloudfall-0.3.0}/.ansible-lint +0 -0
  13. {cloudfall-0.2.0 → cloudfall-0.3.0}/.github/workflows/ci.yml +0 -0
  14. {cloudfall-0.2.0 → cloudfall-0.3.0}/.github/workflows/release.yml +0 -0
  15. {cloudfall-0.2.0 → cloudfall-0.3.0}/.gitignore +0 -0
  16. {cloudfall-0.2.0 → cloudfall-0.3.0}/ARCHITECTURE.md +0 -0
  17. {cloudfall-0.2.0 → cloudfall-0.3.0}/CONTRIBUTING.md +0 -0
  18. {cloudfall-0.2.0 → cloudfall-0.3.0}/LICENSE +0 -0
  19. {cloudfall-0.2.0 → cloudfall-0.3.0}/MANIFESTO.md +0 -0
  20. {cloudfall-0.2.0 → cloudfall-0.3.0}/ROADMAP.md +0 -0
  21. {cloudfall-0.2.0 → cloudfall-0.3.0}/SECURITY.md +0 -0
  22. {cloudfall-0.2.0 → cloudfall-0.3.0}/Taskfile.yml +0 -0
  23. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/README.md +0 -0
  24. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/alert-rules/postgresql-down.yaml +0 -0
  25. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/applications/crm.yaml +0 -0
  26. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/components/crm-backend.yaml +0 -0
  27. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/domains/crm-site.yaml +0 -0
  28. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/logging-stacks/operations.yaml +0 -0
  29. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/operator-policies/production.yaml +0 -0
  30. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/server-types/debian-application.yaml +0 -0
  31. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/servers/h1.yaml +0 -0
  32. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/servers/h2.yaml +0 -0
  33. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/services/postgresql-main.yaml +0 -0
  34. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/services/redis-cache.yaml +0 -0
  35. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
  36. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/alert-rule.schema.json +0 -0
  37. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/application.schema.json +0 -0
  38. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/artifact.schema.json +0 -0
  39. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/backup-receipt.schema.json +0 -0
  40. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/common.schema.json +0 -0
  41. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/component.schema.json +0 -0
  42. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/deployment-receipt.schema.json +0 -0
  43. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/domain.schema.json +0 -0
  44. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/environment-receipt.schema.json +0 -0
  45. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/logging-stack.schema.json +0 -0
  46. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/observed-domain.schema.json +0 -0
  47. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/observed-server.schema.json +0 -0
  48. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/operator-policy.schema.json +0 -0
  49. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/operator-proposal.schema.json +0 -0
  50. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/release-receipt.schema.json +0 -0
  51. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/server-type.schema.json +0 -0
  52. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/server.schema.json +0 -0
  53. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/service.schema.json +0 -0
  54. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/ssh-public-key.schema.json +0 -0
  55. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
  56. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
  57. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
  58. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
  59. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
  60. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/observed/compliant/h1.json +0 -0
  61. {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/observed/compliant/h2.json +0 -0
  62. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/ansible-context.md +0 -0
  63. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/availability-design.md +0 -0
  64. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/brownfield-design.md +0 -0
  65. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
  66. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
  67. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
  68. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/fleet-goals.md +0 -0
  69. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/hybrid-storage-design.md +0 -0
  70. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/landscape.md +0 -0
  71. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/logging-service-guide.md +0 -0
  72. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/neon-migration-guide.md +0 -0
  73. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/new-server-storage-guide.md +0 -0
  74. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/operator-guide.md +0 -0
  75. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
  76. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
  77. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
  78. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
  79. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
  80. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
  81. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
  82. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
  83. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
  84. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
  85. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
  86. {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/secrets-guide.md +0 -0
  87. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/README.md +0 -0
  88. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/ansible.cfg +0 -0
  89. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/backup.yml +0 -0
  90. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/baseline.yml +0 -0
  91. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/bootstrap.yml +0 -0
  92. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/data.yml +0 -0
  93. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/deploy.yml +0 -0
  94. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/domains.yml +0 -0
  95. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/health.yml +0 -0
  96. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/inspect.yml +0 -0
  97. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/logging.yml +0 -0
  98. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/restart.yml +0 -0
  99. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/rollback.yml +0 -0
  100. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/services.yml +0 -0
  101. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/time.yml +0 -0
  102. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
  103. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
  104. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
  105. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
  106. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
  107. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
  108. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
  109. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
  110. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
  111. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
  112. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
  113. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
  114. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
  115. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
  116. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
  117. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
  118. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
  119. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
  120. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
  121. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
  122. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
  123. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
  124. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
  125. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
  126. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
  127. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
  128. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
  129. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
  130. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
  131. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
  132. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
  133. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
  134. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
  135. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
  136. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
  137. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
  138. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
  139. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
  140. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
  141. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
  142. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
  143. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
  144. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
  145. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
  146. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
  147. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
  148. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
  149. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
  150. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
  151. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
  152. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
  153. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
  154. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
  155. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
  156. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
  157. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
  158. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
  159. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
  160. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
  161. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
  162. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
  163. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
  164. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
  165. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
  166. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
  167. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
  168. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
  169. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
  170. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
  171. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
  172. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
  173. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
  174. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
  175. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
  176. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
  177. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
  178. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
  179. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
  180. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
  181. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
  182. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
  183. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
  184. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
  185. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
  186. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
  187. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
  188. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
  189. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
  190. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
  191. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
  192. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
  193. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
  194. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
  195. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/__init__.py +0 -0
  196. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/__main__.py +0 -0
  197. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
  198. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/artifact.py +0 -0
  199. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/cli.py +0 -0
  200. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/playbook.py +0 -0
  201. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/py.typed +0 -0
  202. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_ansible_inventory.py +0 -0
  203. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_artifact_builder.py +0 -0
  204. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_deploy_templates.py +0 -0
  205. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_firewall_templates.py +0 -0
  206. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_logging_templates.py +0 -0
  207. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_nginx_site_templates.py +0 -0
  208. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_playbook.py +0 -0
  209. {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_postgresql_templates.py +0 -0
  210. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/README.md +0 -0
  211. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/environment.md +0 -0
  212. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/findings.md +0 -0
  213. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/issues.md +0 -0
  214. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/linkedin.md +0 -0
  215. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/readiness.md +0 -0
  216. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-agent-dev.md +0 -0
  217. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-dev.md +0 -0
  218. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-index.md +0 -0
  219. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-issues.md +0 -0
  220. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-runtime.md +0 -0
  221. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/trace.md +0 -0
  222. {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/x.md +0 -0
  223. {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/README.md +0 -0
  224. {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/pyproject.toml +0 -0
  225. {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/render.yaml +0 -0
  226. {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/src/demo/__init__.py +0 -0
  227. {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/src/demo/asgi.py +0 -0
  228. {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/uv.lock +0 -0
  229. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/README.md +0 -0
  230. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/__init__.py +0 -0
  231. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/__main__.py +0 -0
  232. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/agent_tools.py +0 -0
  233. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/arguments.py +0 -0
  234. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/audit.py +0 -0
  235. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/authoring.py +0 -0
  236. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/commands.py +0 -0
  237. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/cutover.py +0 -0
  238. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/dashboard.py +0 -0
  239. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/dashboard_server.py +0 -0
  240. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/domain.py +0 -0
  241. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/importer.py +0 -0
  242. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/inventory.py +0 -0
  243. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/lifecycle.py +0 -0
  244. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/mcp_server.py +0 -0
  245. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/migrate.py +0 -0
  246. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/observation.py +0 -0
  247. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/operations.py +0 -0
  248. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/operator.py +0 -0
  249. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/py.typed +0 -0
  250. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/render_api.py +0 -0
  251. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/resources.py +0 -0
  252. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/secrets.py +0 -0
  253. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/service_evidence.py +0 -0
  254. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/validation.py +0 -0
  255. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_arguments.py +0 -0
  256. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_audit.py +0 -0
  257. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_commands.py +0 -0
  258. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_cutover.py +0 -0
  259. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_dashboard_server.py +0 -0
  260. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_data_migration.py +0 -0
  261. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_importer.py +0 -0
  262. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_inventory.py +0 -0
  263. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_lifecycle.py +0 -0
  264. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_migrate.py +0 -0
  265. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_operations_dashboard.py +0 -0
  266. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_operator.py +0 -0
  267. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_render_api.py +0 -0
  268. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_resources.py +0 -0
  269. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_secrets.py +0 -0
  270. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_service_evidence.py +0 -0
  271. {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_validation.py +0 -0
  272. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/apple-touch-icon.png +0 -0
  273. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/favicon-32.png +0 -0
  274. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/favicon.svg +0 -0
  275. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/icon-192.png +0 -0
  276. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/icon-512.png +0 -0
  277. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/index.html +0 -0
  278. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/manifesto.html +0 -0
  279. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/og.png +0 -0
  280. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/site.webmanifest +0 -0
  281. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/src/worker.js +0 -0
  282. {cloudfall-0.2.0 → cloudfall-0.3.0}/site/wrangler.jsonc +0 -0
@@ -6,6 +6,49 @@ Notable changes to Cloudfall. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] — 2026-09-20
10
+
11
+ ### Removed
12
+
13
+ - `cloudfall init --rev` and `--source`, and the machinery behind them:
14
+ `GitRevision`, `GitSourceUrl`, `GitPin`, `IndexPin`, `CheckoutState`, the
15
+ checkout inspection, and the `project_revision_unresolved`,
16
+ `project_revision_uncommitted` and `project_revision_unpublished` errors.
17
+ Cloudfall is on PyPI, so a project pins a release and resolves it like any
18
+ other dependency; `resolve_installed_version` replaces
19
+ `resolve_installed_pin`, and the `init` envelope reports `version` rather
20
+ than a `pin` object. To run an unreleased Cloudfall in a project, point the
21
+ dependency at a checkout with uv's own `[tool.uv.sources]`
22
+
23
+ ### Changed
24
+
25
+ - The quickstart and the Render migration guide install Cloudfall from the
26
+ index rather than from git
27
+
28
+ ## [0.2.1] — 2026-09-20
29
+
30
+ ### Fixed
31
+
32
+ - `cloudfall init` works from a PyPI install. An install from the package
33
+ index records no origin to read a commit from, so `init` failed with
34
+ `project_revision_unresolved` on the first command a new user runs, and
35
+ `--rev` only worked around it by writing a project that installed
36
+ Cloudfall from git instead of from the index. A project now pins whatever
37
+ ran `init`: `cloudfall==<version>` with no `[tool.uv.sources]` table for a
38
+ released install, the commit and repository for a git or source install
39
+ - The project README links the secrets guide and the reference examples at
40
+ the release tag when the pin is a released version, rather than at a
41
+ commit the install does not know
42
+
43
+ ### Changed
44
+
45
+ - `InitOptions` and the `cloudfall init` envelope carry one `pin` instead of
46
+ a `revision` and a `source`: `{"kind": "index", "version": ...}` or
47
+ `{"kind": "git", "revision": ..., "source": ...}`. `resolve_installed_pin`
48
+ replaces `resolve_installed_revision`
49
+ - `--rev` documents that it pins a commit from `--source`, which is now the
50
+ explicit alternative to the default rather than the only mechanism
51
+
9
52
  ## [0.2.0] — 2026-09-20
10
53
 
11
54
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cloudfall
3
- Version: 0.2.0
3
+ Version: 0.3.0
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
@@ -160,10 +160,18 @@ is implemented yet; everything below this line is.
160
160
 
161
161
  ## Quickstart
162
162
 
163
- Cloudfall is not on PyPI yet; run it from a clone of this repository. The
164
- only prerequisite is [`uv`](https://docs.astral.sh/uv/): it provisions the
165
- required Python 3.14 interpreter and all dependencies automatically, so you
166
- do not need Python 3.14 preinstalled.
163
+ Cloudfall is on PyPI. The only prerequisite is
164
+ [`uv`](https://docs.astral.sh/uv/): it provisions the required Python 3.14
165
+ interpreter and all dependencies automatically, so you do not need Python
166
+ 3.14 preinstalled.
167
+
168
+ ```console
169
+ uv tool install cloudfall
170
+ cloudfall --help
171
+ ```
172
+
173
+ The reference resource set lives in this repository, so validating it means
174
+ a clone:
167
175
 
168
176
  ```console
169
177
  git clone https://github.com/romamo/cloudfall.git
@@ -189,13 +197,12 @@ uv run cloudfall-engine inventory render --project config/examples
189
197
  Your servers, applications, evidence, and any playbooks of your own belong in
190
198
  a project: a directory of its own, kept private. Cloudfall's wheel bundles the
191
199
  schema catalog and the Ansible engine, so the project needs no checkout of
192
- this repository. `cloudfall init` is the first command; run it straight from
193
- git or from a clone (`uv run cloudfall init ../my-project`), and it pins the
194
- project to the exact Cloudfall commit it ran from. Without a directory
195
- argument it initializes the current directory, which must be empty:
200
+ this repository. `cloudfall init` is the first command, and it pins the
201
+ project to the released Cloudfall that ran it. Without a directory argument
202
+ it initializes the current directory, which must be empty:
196
203
 
197
204
  ```console
198
- uvx --from git+https://github.com/romamo/cloudfall.git cloudfall init my-project
205
+ uvx cloudfall init my-project
199
206
  cd my-project
200
207
  uv sync
201
208
  ```
@@ -215,15 +222,18 @@ repository, and a `pyproject.toml`:
215
222
  name = "my-project"
216
223
  version = "0"
217
224
  requires-python = ">=3.14"
218
- dependencies = ["cloudfall"]
225
+ dependencies = ["cloudfall==<version>"]
219
226
 
220
227
  [tool.uv]
221
228
  package = false
222
-
223
- [tool.uv.sources]
224
- cloudfall = { git = "https://github.com/romamo/cloudfall.git", rev = "<commit>" }
225
229
  ```
226
230
 
231
+ Bump that version and run `uv sync` to move the project to a newer
232
+ Cloudfall. To run an unreleased Cloudfall in a project, point the dependency
233
+ at a checkout or a commit with uv's own
234
+ [`[tool.uv.sources]`](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-sources);
235
+ Cloudfall itself only ever pins a release.
236
+
227
237
  Only the kind directories are read as resources, so playbooks, roles, docs,
228
238
  and tooling files may live anywhere else in the project. Commands find the
229
239
  project on their own: the current directory when it is one, else
@@ -134,10 +134,18 @@ is implemented yet; everything below this line is.
134
134
 
135
135
  ## Quickstart
136
136
 
137
- Cloudfall is not on PyPI yet; run it from a clone of this repository. The
138
- only prerequisite is [`uv`](https://docs.astral.sh/uv/): it provisions the
139
- required Python 3.14 interpreter and all dependencies automatically, so you
140
- do not need Python 3.14 preinstalled.
137
+ Cloudfall is on PyPI. The only prerequisite is
138
+ [`uv`](https://docs.astral.sh/uv/): it provisions the required Python 3.14
139
+ interpreter and all dependencies automatically, so you do not need Python
140
+ 3.14 preinstalled.
141
+
142
+ ```console
143
+ uv tool install cloudfall
144
+ cloudfall --help
145
+ ```
146
+
147
+ The reference resource set lives in this repository, so validating it means
148
+ a clone:
141
149
 
142
150
  ```console
143
151
  git clone https://github.com/romamo/cloudfall.git
@@ -163,13 +171,12 @@ uv run cloudfall-engine inventory render --project config/examples
163
171
  Your servers, applications, evidence, and any playbooks of your own belong in
164
172
  a project: a directory of its own, kept private. Cloudfall's wheel bundles the
165
173
  schema catalog and the Ansible engine, so the project needs no checkout of
166
- this repository. `cloudfall init` is the first command; run it straight from
167
- git or from a clone (`uv run cloudfall init ../my-project`), and it pins the
168
- project to the exact Cloudfall commit it ran from. Without a directory
169
- argument it initializes the current directory, which must be empty:
174
+ this repository. `cloudfall init` is the first command, and it pins the
175
+ project to the released Cloudfall that ran it. Without a directory argument
176
+ it initializes the current directory, which must be empty:
170
177
 
171
178
  ```console
172
- uvx --from git+https://github.com/romamo/cloudfall.git cloudfall init my-project
179
+ uvx cloudfall init my-project
173
180
  cd my-project
174
181
  uv sync
175
182
  ```
@@ -189,15 +196,18 @@ repository, and a `pyproject.toml`:
189
196
  name = "my-project"
190
197
  version = "0"
191
198
  requires-python = ">=3.14"
192
- dependencies = ["cloudfall"]
199
+ dependencies = ["cloudfall==<version>"]
193
200
 
194
201
  [tool.uv]
195
202
  package = false
196
-
197
- [tool.uv.sources]
198
- cloudfall = { git = "https://github.com/romamo/cloudfall.git", rev = "<commit>" }
199
203
  ```
200
204
 
205
+ Bump that version and run `uv sync` to move the project to a newer
206
+ Cloudfall. To run an unreleased Cloudfall in a project, point the dependency
207
+ at a checkout or a commit with uv's own
208
+ [`[tool.uv.sources]`](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-sources);
209
+ Cloudfall itself only ever pins a release.
210
+
201
211
  Only the kind directories are read as resources, so playbooks, roles, docs,
202
212
  and tooling files may live anywhere else in the project. Commands find the
203
213
  project on their own: the current directory when it is one, else
@@ -55,7 +55,7 @@ Not imported today; each lands in the gap report instead of being guessed at:
55
55
  - Your `render.yaml` blueprint and access to your application repositories
56
56
  - Python 3.14 and [`uv`](https://docs.astral.sh/uv/) on the machine you run
57
57
  Cloudfall from (your workstation works; no management server is required)
58
- - A project: `uvx --from git+https://github.com/romamo/cloudfall.git cloudfall init my-project`,
58
+ - A project: `uvx cloudfall init my-project`,
59
59
  then `cd my-project && uv sync`. Every command below runs from inside it
60
60
 
61
61
  ## Step 1: import the blueprint
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cloudfall"
3
- version = "0.2.0"
3
+ version = "0.3.0"
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"
@@ -96,17 +96,14 @@ from cloudfall.operator import (
96
96
  run_once as operator_run_once,
97
97
  )
98
98
  from cloudfall.project import (
99
- DEFAULT_SOURCE_URL,
100
99
  PROJECT_DIRECTORY_VARIABLE,
101
- GitRevision,
102
- GitSourceUrl,
103
100
  InitOptions,
104
101
  ProjectDescription,
105
102
  ProjectError,
106
103
  ProjectName,
107
104
  init_project,
108
105
  project_context,
109
- resolve_installed_revision,
106
+ resolve_installed_version,
110
107
  )
111
108
  from cloudfall.render_api import (
112
109
  HttpRenderApiClient,
@@ -199,13 +196,6 @@ def _add_init_parser(
199
196
  "--name",
200
197
  help="package name written to pyproject.toml (default: the directory name)",
201
198
  )
202
- init_parser.add_argument(
203
- "--rev",
204
- help=(
205
- "Cloudfall commit to pin (default: the commit the running "
206
- "cloudfall was installed from)"
207
- ),
208
- )
209
199
  init_parser.add_argument(
210
200
  "--description",
211
201
  help=(
@@ -213,13 +203,6 @@ def _add_init_parser(
213
203
  "and pyproject.toml"
214
204
  ),
215
205
  )
216
- init_parser.add_argument(
217
- "--source",
218
- default=DEFAULT_SOURCE_URL,
219
- help=(
220
- f"git location of Cloudfall to install from (default: {DEFAULT_SOURCE_URL})"
221
- ),
222
- )
223
206
 
224
207
 
225
208
  def _add_add_parsers(
@@ -1030,16 +1013,10 @@ def _run_init(arguments: Namespace) -> int:
1030
1013
  if arguments.name is not None
1031
1014
  else ProjectName.from_directory(directory)
1032
1015
  )
1033
- revision = (
1034
- GitRevision.from_boundary(arguments.rev)
1035
- if arguments.rev is not None
1036
- else resolve_installed_revision()
1037
- )
1038
1016
  options = InitOptions(
1039
1017
  directory=directory,
1040
1018
  name=name,
1041
- revision=revision,
1042
- source=GitSourceUrl.from_boundary(arguments.source),
1019
+ version=resolve_installed_version(),
1043
1020
  description=(
1044
1021
  ProjectDescription.from_boundary(arguments.description)
1045
1022
  if arguments.description is not None
@@ -4,7 +4,7 @@ A project is one directory holding everything Cloudfall needs to run one
4
4
  user's stack: the fleet (servers, server types, SSH public keys), the
5
5
  applications (applications, components, services, domains), the operating
6
6
  declarations (alert rules, operator policies, logging stacks), and the
7
- ``pyproject.toml`` that pins the Cloudfall revision the project runs with.
7
+ ``pyproject.toml`` that pins the released Cloudfall the project runs with.
8
8
  Every ``cloudfall`` command runs inside a project: the current directory
9
9
  when it is one, else ``--project``, else ``CLOUDFALL_PROJECT``.
10
10
  Evidence, receipts, and rendered files land under ``tmp/`` inside the
@@ -13,7 +13,6 @@ project and are never committed.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- import json
17
16
  import os
18
17
  import re
19
18
  import shutil
@@ -25,7 +24,6 @@ from enum import Enum
25
24
  from importlib import metadata
26
25
  from pathlib import Path
27
26
  from typing import TYPE_CHECKING
28
- from urllib.parse import urlparse
29
27
 
30
28
  from cloudfall.commands import CommandEffect, commands_with_effect
31
29
  from cloudfall.domain import ResourceKind
@@ -33,17 +31,17 @@ from cloudfall.domain import ResourceKind
33
31
  if TYPE_CHECKING:
34
32
  from collections.abc import Callable, Iterator, Mapping
35
33
 
36
- _GIT_REVISION_PATTERN = re.compile(r"^[0-9a-f]{40}$")
37
34
  _PROJECT_NAME_PATTERN = re.compile(r"^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$")
38
- _SCP_LIKE_SOURCE_PATTERN = re.compile(
39
- r"^(?P<user>[\w.-]+)@(?P<host>[\w.-]+):(?P<path>[\w./-]+)$"
35
+ _RELEASE_VERSION_PATTERN = re.compile(
36
+ r"^\d+(?:\.\d+)*(?:(?:a|b|rc)\d+)?(?:\.post\d+)?(?:\.dev\d+)?$"
40
37
  )
41
38
  _PROJECT_NAME_MAX_LENGTH = 64
42
39
  _PROJECT_DESCRIPTION_MAX_LENGTH = 512
43
40
  _GIT_TIMEOUT_SECONDS = 30
44
41
  _GIT_NOT_A_REPOSITORY_EXIT_CODE = 128
45
42
 
46
- DEFAULT_SOURCE_URL = "https://github.com/romamo/cloudfall.git"
43
+ REPOSITORY_URL = "https://github.com/romamo/cloudfall"
44
+ """Location of the Cloudfall repository, for links to its files."""
47
45
 
48
46
  SECRETS_DIRECTORY = "secrets"
49
47
  """Project directory holding sops-encrypted secret fragments."""
@@ -66,9 +64,7 @@ CLAUDE_POINTER_FILE = "CLAUDE.md"
66
64
  _ERROR_NAME_UNDERIVED = "project_name_underived"
67
65
  _ERROR_DIRECTORY_MISSING = "project_directory_missing"
68
66
  _ERROR_DIRECTORY_UNRESOLVED = "project_directory_unresolved"
69
- _ERROR_REVISION_UNRESOLVED = "project_revision_unresolved"
70
- _ERROR_REVISION_UNCOMMITTED = "project_revision_uncommitted"
71
- _ERROR_REVISION_UNPUBLISHED = "project_revision_unpublished"
67
+ _ERROR_VERSION_UNRESOLVED = "project_version_unresolved"
72
68
  _ERROR_NOT_A_DIRECTORY = "project_directory_not_a_directory"
73
69
  _ERROR_NOT_EMPTY = "project_directory_not_empty"
74
70
  _ERROR_GIT_UNAVAILABLE = "project_git_unavailable"
@@ -100,72 +96,27 @@ class ProjectError(RuntimeError):
100
96
 
101
97
 
102
98
  @dataclass(frozen=True, slots=True)
103
- class GitRevision:
104
- """Full 40-character commit hash a project pins Cloudfall to."""
99
+ class ReleaseVersion:
100
+ """Released Cloudfall version a project pins from the package index."""
105
101
 
106
102
  value: str
107
103
 
108
104
  def __post_init__(self) -> None:
109
- """Enforce the full-hash invariant at construction time."""
110
- if not _GIT_REVISION_PATTERN.fullmatch(self.value):
111
- message = f"git revision must be a full 40-hex commit hash: {self.value!r}"
105
+ """Enforce the release-version invariant at construction time."""
106
+ if not _RELEASE_VERSION_PATTERN.fullmatch(self.value):
107
+ message = f"release version must be a PEP 440 release: {self.value!r}"
112
108
  raise ValueError(message)
113
109
 
114
110
  @classmethod
115
- def from_boundary(cls, value: object) -> GitRevision:
111
+ def from_boundary(cls, value: object) -> ReleaseVersion:
116
112
  """Coerce a boundary value while keeping internal APIs strictly typed."""
117
113
  if not isinstance(value, str):
118
- message = f"git revision must be a string, got {type(value).__name__}"
119
- raise TypeError(message)
120
- return cls(value.strip().lower())
121
-
122
- def __str__(self) -> str:
123
- """Return the serialized hash."""
124
- return self.value
125
-
126
-
127
- @dataclass(frozen=True, slots=True)
128
- class GitSourceUrl:
129
- """Location of the Cloudfall repository a project installs from."""
130
-
131
- value: str
132
-
133
- def __post_init__(self) -> None:
134
- """Accept https and ssh git locations only."""
135
- parsed = urlparse(self.value)
136
- https = parsed.scheme == "https" and bool(parsed.netloc)
137
- ssh = parsed.scheme == "ssh" and bool(parsed.netloc)
138
- scp_like = _SCP_LIKE_SOURCE_PATTERN.fullmatch(self.value) is not None
139
- if not (https or ssh or scp_like):
140
- message = f"git source must be an https or ssh location: {self.value!r}"
141
- raise ValueError(message)
142
-
143
- @classmethod
144
- def from_boundary(cls, value: object) -> GitSourceUrl:
145
- """Coerce a boundary value while keeping internal APIs strictly typed."""
146
- if not isinstance(value, str):
147
- message = f"git source must be a string, got {type(value).__name__}"
114
+ message = f"release version must be a string, got {type(value).__name__}"
148
115
  raise TypeError(message)
149
116
  return cls(value.strip())
150
117
 
151
- @property
152
- def browse_url(self) -> str:
153
- """Return the https location of the repository for links to its files.
154
-
155
- ``https://host/owner/repo.git``, ``ssh://git@host/owner/repo.git``, and
156
- ``git@host:owner/repo.git`` all browse at ``https://host/owner/repo``.
157
- """
158
- scp_like = _SCP_LIKE_SOURCE_PATTERN.fullmatch(self.value)
159
- if scp_like is not None:
160
- host, path = scp_like.group("host"), scp_like.group("path")
161
- else:
162
- parsed = urlparse(self.value)
163
- host, path = parsed.hostname or "", parsed.path
164
- path = path.strip("/").removesuffix(".git")
165
- return f"https://{host}/{path}"
166
-
167
118
  def __str__(self) -> str:
168
- """Return the serialized location."""
119
+ """Return the serialized version."""
169
120
  return self.value
170
121
 
171
122
 
@@ -255,8 +206,7 @@ class InitOptions:
255
206
 
256
207
  directory: Path
257
208
  name: ProjectName
258
- revision: GitRevision
259
- source: GitSourceUrl = GitSourceUrl(DEFAULT_SOURCE_URL)
209
+ version: ReleaseVersion
260
210
  description: ProjectDescription | None = None
261
211
 
262
212
 
@@ -276,8 +226,7 @@ class ProjectScaffold:
276
226
 
277
227
  directory: Path
278
228
  name: ProjectName
279
- revision: GitRevision
280
- source: GitSourceUrl
229
+ version: ReleaseVersion
281
230
  files: tuple[str, ...]
282
231
  git: GitSetup
283
232
 
@@ -288,8 +237,7 @@ class ProjectScaffold:
288
237
  "project": {
289
238
  "directory": str(self.directory),
290
239
  "name": str(self.name),
291
- "revision": str(self.revision),
292
- "source": str(self.source),
240
+ "version": str(self.version),
293
241
  "git": self.git.value,
294
242
  },
295
243
  "files": list(self.files),
@@ -302,17 +250,6 @@ class ProjectScaffold:
302
250
  }
303
251
 
304
252
 
305
- @dataclass(frozen=True, slots=True)
306
- class CheckoutState:
307
- """What a source checkout says about the commit it is running."""
308
-
309
- head: str
310
- clean: bool
311
- """No tracked file differs from ``HEAD``, so ``HEAD`` is the running code."""
312
- published: bool
313
- """``HEAD`` is reachable from a remote-tracking branch, so it can be fetched."""
314
-
315
-
316
253
  def is_project(directory: Path) -> bool:
317
254
  """Return whether a directory holds at least one resource kind directory."""
318
255
  return directory.is_dir() and any(
@@ -388,69 +325,30 @@ def project_path(value: str) -> Path:
388
325
  return path
389
326
 
390
327
 
391
- def resolve_installed_revision(
392
- read_direct_url: Callable[[], str | None] | None = None,
393
- inspect_checkout: Callable[[Path], CheckoutState] | None = None,
394
- ) -> GitRevision:
395
- """Return the commit the running Cloudfall was installed from.
328
+ def resolve_installed_version(
329
+ read_version: Callable[[], str] | None = None,
330
+ ) -> ReleaseVersion:
331
+ """Return the released version of the running Cloudfall.
396
332
 
397
- A wheel installed from git records its commit in ``direct_url.json``; an
398
- editable source checkout records the checkout location, whose ``HEAD`` is
399
- the commit, provided the checkout is clean (else ``HEAD`` is not the code
400
- that is running) and ``HEAD`` is on a remote branch (else ``uv sync``
401
- cannot fetch it). Anything else cannot be pinned and must be given
402
- explicitly.
333
+ A project depends on ``cloudfall==<version>`` and resolves it from the
334
+ package index like any other dependency. An install whose version is not
335
+ a release cannot be pinned that way, which is a checkout of this
336
+ repository between releases.
403
337
  """
404
- direct_url = (read_direct_url or _installed_direct_url)()
405
- if direct_url is None:
406
- message = (
407
- "the installed cloudfall distribution records no origin; "
408
- "pass --rev with the commit to pin"
409
- )
410
- raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
411
- record = json.loads(direct_url)
412
- if not isinstance(record, dict):
413
- message = f"direct_url.json is not an object: {direct_url!r}"
414
- raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
415
- vcs_info = record.get("vcs_info")
416
- if isinstance(vcs_info, dict) and "commit_id" in vcs_info:
417
- return GitRevision.from_boundary(vcs_info["commit_id"])
418
- dir_info = record.get("dir_info")
419
- url = record.get("url")
420
- if (
421
- isinstance(dir_info, dict)
422
- and isinstance(url, str)
423
- and url.startswith("file://")
424
- ):
425
- checkout = Path(urlparse(url).path)
426
- inspect = inspect_checkout or _inspect_checkout
427
- return _pinnable_head(checkout, inspect(checkout))
428
- message = (
429
- "the installed cloudfall distribution is neither a git install nor a "
430
- f"source checkout ({url!r}); pass --rev with the commit to pin"
431
- )
432
- raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
433
-
434
-
435
- def _pinnable_head(checkout: Path, state: CheckoutState) -> GitRevision:
436
- head = GitRevision.from_boundary(state.head)
437
- if not state.clean:
438
- message = (
439
- f"the source checkout {checkout} has uncommitted changes, so HEAD "
440
- f"{head} is not the code that is running; commit them or pass --rev"
441
- )
442
- raise ProjectError(_ERROR_REVISION_UNCOMMITTED, message)
443
- if not state.published:
338
+ raw = (read_version or _installed_version)()
339
+ try:
340
+ return ReleaseVersion.from_boundary(raw)
341
+ except ValueError as error:
444
342
  message = (
445
- f"HEAD {head} of the source checkout {checkout} is on no remote "
446
- "branch, so `uv sync` could not fetch it; push it or pass --rev"
343
+ f"the installed cloudfall version {raw!r} is not a release, so a "
344
+ "project cannot pin it; install a released cloudfall, or set the "
345
+ "dependency in the project's pyproject.toml by hand"
447
346
  )
448
- raise ProjectError(_ERROR_REVISION_UNPUBLISHED, message)
449
- return head
347
+ raise ProjectError(_ERROR_VERSION_UNRESOLVED, message) from error
450
348
 
451
349
 
452
- def _installed_direct_url() -> str | None:
453
- return metadata.distribution("cloudfall").read_text("direct_url.json")
350
+ def _installed_version() -> str:
351
+ return metadata.version("cloudfall")
454
352
 
455
353
 
456
354
  def _git_binary() -> str:
@@ -486,15 +384,6 @@ def _git_or_fail(directory: Path, *arguments: str) -> str:
486
384
  return completed.stdout
487
385
 
488
386
 
489
- def _inspect_checkout(checkout: Path) -> CheckoutState:
490
- head = _git_or_fail(checkout, "rev-parse", "HEAD").strip()
491
- changes = _git_or_fail(checkout, "status", "--porcelain", "--untracked-files=no")
492
- remote_branches = _git_or_fail(checkout, "branch", "--remotes", "--contains", head)
493
- return CheckoutState(
494
- head=head, clean=not changes.strip(), published=bool(remote_branches.strip())
495
- )
496
-
497
-
498
387
  def _initialize_git(directory: Path) -> GitSetup:
499
388
  """Create a repository in the directory unless one already encloses it."""
500
389
  enclosing = _run_git(directory, "rev-parse", "--is-inside-work-tree")
@@ -543,8 +432,7 @@ def init_project(
543
432
  return ProjectScaffold(
544
433
  directory=directory,
545
434
  name=options.name,
546
- revision=options.revision,
547
- source=options.source,
435
+ version=options.version,
548
436
  files=tuple(files),
549
437
  git=git,
550
438
  )
@@ -561,17 +449,14 @@ name = "{options.name}"
561
449
  version = "0"
562
450
  description = "{description}"
563
451
  requires-python = ">=3.14"
564
- dependencies = ["cloudfall"]
452
+ # Cloudfall is an installed package: the schema catalog and the Ansible engine
453
+ # ship inside the wheel. Bump this version to move the project to a newer
454
+ # Cloudfall, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
455
+ # the `cloudfall-mcp` agent server.
456
+ dependencies = ["cloudfall=={options.version}"]
565
457
 
566
458
  [tool.uv]
567
459
  package = false
568
-
569
- # Cloudfall is an installed package: the schema catalog and the Ansible engine
570
- # ship inside the wheel. Bump `rev` to move this project to a newer Cloudfall
571
- # commit, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
572
- # the `cloudfall-mcp` agent server.
573
- [tool.uv.sources]
574
- cloudfall = {{ git = "{options.source}", rev = "{options.revision}" }}
575
460
  """
576
461
 
577
462
 
@@ -603,10 +488,10 @@ creation_rules:
603
488
  def _readme(options: InitOptions, git: GitSetup) -> str:
604
489
  tmp = RUNTIME_DIRECTORY
605
490
  secrets = SECRETS_DIRECTORY
606
- browse = options.source.browse_url
607
- revision = options.revision
608
- secrets_guide = f"{browse}/blob/{revision}/{SECRETS_GUIDE_PATH}"
609
- examples = f"{browse}/tree/{revision}/{EXAMPLES_PATH}"
491
+ browse = REPOSITORY_URL
492
+ ref = f"v{options.version}"
493
+ secrets_guide = f"{browse}/blob/{ref}/{SECRETS_GUIDE_PATH}"
494
+ examples = f"{browse}/tree/{ref}/{EXAMPLES_PATH}"
610
495
  about = (
611
496
  str(options.description)
612
497
  if options.description is not None
@@ -689,9 +574,9 @@ uv run cloudfall migrate --build <component>=main
689
574
  uv run cloudfall migrate --build <component>=main --yes
690
575
  ```
691
576
 
692
- Cloudfall is pinned to one commit in `pyproject.toml`; bump `rev` and run
693
- `uv sync` to move the project to a newer release. The links below point at
694
- the pinned commit.
577
+ Cloudfall is pinned to one released version in `pyproject.toml`; bump it
578
+ and run `uv sync` to move the project to a newer release. The links below
579
+ point at the pinned release.
695
580
 
696
581
  [cloudfall]: {browse}
697
582
  [secrets-guide]: {secrets_guide}
@@ -8,7 +8,12 @@ from pathlib import Path
8
8
  import pytest
9
9
  import yaml
10
10
  from cloudfall.agent_tools import AgentConfig, AgentToolset
11
- from cloudfall.project import GitRevision, InitOptions, ProjectName, init_project
11
+ from cloudfall.project import (
12
+ InitOptions,
13
+ ProjectName,
14
+ ReleaseVersion,
15
+ init_project,
16
+ )
12
17
 
13
18
  ROOT = Path(__file__).parents[2]
14
19
  SCHEMAS = ROOT / "config" / "schemas" / "v1"
@@ -26,7 +31,7 @@ services:
26
31
  """
27
32
 
28
33
 
29
- REVISION = "5dea76ae7fdf4f99f5e8eb84b939dfa68b12ef3b"
34
+ VERSION = "0.3.0"
30
35
 
31
36
 
32
37
  def _config(
@@ -141,7 +146,7 @@ def _fresh_project(tmp_path: Path) -> AgentToolset:
141
146
  InitOptions(
142
147
  directory=directory,
143
148
  name=ProjectName("project"),
144
- revision=GitRevision(REVISION),
149
+ version=ReleaseVersion(VERSION),
145
150
  )
146
151
  )
147
152
  return AgentToolset(_config(tmp_path, project_directory=directory))