cloudfall 0.2.1__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.1 → cloudfall-0.3.0}/CHANGELOG.md +19 -0
  2. {cloudfall-0.2.1 → cloudfall-0.3.0}/PKG-INFO +8 -9
  3. {cloudfall-0.2.1 → cloudfall-0.3.0}/README.md +7 -8
  4. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/render-migration-guide.md +1 -1
  5. {cloudfall-0.2.1 → cloudfall-0.3.0}/pyproject.toml +1 -1
  6. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/cli.py +2 -30
  7. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/project.py +26 -260
  8. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_agent_tools.py +3 -4
  9. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_authoring.py +3 -4
  10. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_project.py +44 -176
  11. {cloudfall-0.2.1 → cloudfall-0.3.0}/uv.lock +1 -1
  12. {cloudfall-0.2.1 → cloudfall-0.3.0}/.ansible-lint +0 -0
  13. {cloudfall-0.2.1 → cloudfall-0.3.0}/.github/workflows/ci.yml +0 -0
  14. {cloudfall-0.2.1 → cloudfall-0.3.0}/.github/workflows/release.yml +0 -0
  15. {cloudfall-0.2.1 → cloudfall-0.3.0}/.gitignore +0 -0
  16. {cloudfall-0.2.1 → cloudfall-0.3.0}/ARCHITECTURE.md +0 -0
  17. {cloudfall-0.2.1 → cloudfall-0.3.0}/CONTRIBUTING.md +0 -0
  18. {cloudfall-0.2.1 → cloudfall-0.3.0}/LICENSE +0 -0
  19. {cloudfall-0.2.1 → cloudfall-0.3.0}/MANIFESTO.md +0 -0
  20. {cloudfall-0.2.1 → cloudfall-0.3.0}/ROADMAP.md +0 -0
  21. {cloudfall-0.2.1 → cloudfall-0.3.0}/SECURITY.md +0 -0
  22. {cloudfall-0.2.1 → cloudfall-0.3.0}/Taskfile.yml +0 -0
  23. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/README.md +0 -0
  24. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/alert-rules/postgresql-down.yaml +0 -0
  25. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/applications/crm.yaml +0 -0
  26. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/components/crm-backend.yaml +0 -0
  27. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/domains/crm-site.yaml +0 -0
  28. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/logging-stacks/operations.yaml +0 -0
  29. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/operator-policies/production.yaml +0 -0
  30. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/server-types/debian-application.yaml +0 -0
  31. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/servers/h1.yaml +0 -0
  32. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/servers/h2.yaml +0 -0
  33. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/services/postgresql-main.yaml +0 -0
  34. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/services/redis-cache.yaml +0 -0
  35. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
  36. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/alert-rule.schema.json +0 -0
  37. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/application.schema.json +0 -0
  38. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/artifact.schema.json +0 -0
  39. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/backup-receipt.schema.json +0 -0
  40. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/common.schema.json +0 -0
  41. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/component.schema.json +0 -0
  42. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/deployment-receipt.schema.json +0 -0
  43. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/domain.schema.json +0 -0
  44. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/environment-receipt.schema.json +0 -0
  45. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/logging-stack.schema.json +0 -0
  46. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/observed-domain.schema.json +0 -0
  47. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/observed-server.schema.json +0 -0
  48. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/operator-policy.schema.json +0 -0
  49. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/operator-proposal.schema.json +0 -0
  50. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/release-receipt.schema.json +0 -0
  51. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/server-type.schema.json +0 -0
  52. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/server.schema.json +0 -0
  53. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/service.schema.json +0 -0
  54. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/schemas/v1/ssh-public-key.schema.json +0 -0
  55. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
  56. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
  57. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
  58. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
  59. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
  60. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/tests/observed/compliant/h1.json +0 -0
  61. {cloudfall-0.2.1 → cloudfall-0.3.0}/config/tests/observed/compliant/h2.json +0 -0
  62. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/ansible-context.md +0 -0
  63. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/availability-design.md +0 -0
  64. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/brownfield-design.md +0 -0
  65. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
  66. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
  67. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
  68. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/fleet-goals.md +0 -0
  69. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/hybrid-storage-design.md +0 -0
  70. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/landscape.md +0 -0
  71. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/logging-service-guide.md +0 -0
  72. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/neon-migration-guide.md +0 -0
  73. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/new-server-storage-guide.md +0 -0
  74. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/operator-guide.md +0 -0
  75. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
  76. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
  77. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
  78. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
  79. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
  80. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
  81. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
  82. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
  83. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
  84. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
  85. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
  86. {cloudfall-0.2.1 → cloudfall-0.3.0}/docs/secrets-guide.md +0 -0
  87. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/README.md +0 -0
  88. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/ansible.cfg +0 -0
  89. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/backup.yml +0 -0
  90. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/baseline.yml +0 -0
  91. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/bootstrap.yml +0 -0
  92. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/data.yml +0 -0
  93. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/deploy.yml +0 -0
  94. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/domains.yml +0 -0
  95. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/health.yml +0 -0
  96. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/inspect.yml +0 -0
  97. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/logging.yml +0 -0
  98. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/restart.yml +0 -0
  99. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/rollback.yml +0 -0
  100. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/services.yml +0 -0
  101. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/playbooks/time.yml +0 -0
  102. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
  103. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
  104. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
  105. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
  106. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
  107. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
  108. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
  109. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
  110. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
  111. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
  112. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
  113. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
  114. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
  115. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
  116. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
  117. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
  118. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
  119. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
  120. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
  121. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
  122. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
  123. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
  124. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
  125. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
  126. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
  127. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
  128. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
  129. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
  130. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
  131. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
  132. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
  133. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
  134. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
  135. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
  136. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
  137. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
  138. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
  139. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
  140. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
  141. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
  142. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
  143. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
  144. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
  145. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
  146. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
  147. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
  148. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
  149. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
  150. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
  151. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
  152. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
  153. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
  154. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
  155. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
  156. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
  157. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
  158. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
  159. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
  160. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
  161. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
  162. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
  163. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
  164. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
  165. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
  166. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
  167. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
  168. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
  169. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
  170. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
  171. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
  172. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
  173. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
  174. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
  175. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
  176. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
  177. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
  178. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
  179. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
  180. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
  181. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
  182. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
  183. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
  184. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
  185. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
  186. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
  187. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
  188. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
  189. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
  190. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
  191. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
  192. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
  193. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
  194. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
  195. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/src/cloudfall_engine/__init__.py +0 -0
  196. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/src/cloudfall_engine/__main__.py +0 -0
  197. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
  198. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/src/cloudfall_engine/artifact.py +0 -0
  199. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/src/cloudfall_engine/cli.py +0 -0
  200. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/src/cloudfall_engine/playbook.py +0 -0
  201. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/src/cloudfall_engine/py.typed +0 -0
  202. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/tests/test_ansible_inventory.py +0 -0
  203. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/tests/test_artifact_builder.py +0 -0
  204. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/tests/test_deploy_templates.py +0 -0
  205. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/tests/test_firewall_templates.py +0 -0
  206. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/tests/test_logging_templates.py +0 -0
  207. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/tests/test_nginx_site_templates.py +0 -0
  208. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/tests/test_playbook.py +0 -0
  209. {cloudfall-0.2.1 → cloudfall-0.3.0}/engine/tests/test_postgresql_templates.py +0 -0
  210. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/README.md +0 -0
  211. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/environment.md +0 -0
  212. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/findings.md +0 -0
  213. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/issues.md +0 -0
  214. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/linkedin.md +0 -0
  215. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/readiness.md +0 -0
  216. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/report-agent-dev.md +0 -0
  217. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/report-dev.md +0 -0
  218. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/report-index.md +0 -0
  219. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/report-issues.md +0 -0
  220. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/report-runtime.md +0 -0
  221. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/trace.md +0 -0
  222. {cloudfall-0.2.1 → cloudfall-0.3.0}/evaluations/cloudfall/x.md +0 -0
  223. {cloudfall-0.2.1 → cloudfall-0.3.0}/examples/render-demo/README.md +0 -0
  224. {cloudfall-0.2.1 → cloudfall-0.3.0}/examples/render-demo/pyproject.toml +0 -0
  225. {cloudfall-0.2.1 → cloudfall-0.3.0}/examples/render-demo/render.yaml +0 -0
  226. {cloudfall-0.2.1 → cloudfall-0.3.0}/examples/render-demo/src/demo/__init__.py +0 -0
  227. {cloudfall-0.2.1 → cloudfall-0.3.0}/examples/render-demo/src/demo/asgi.py +0 -0
  228. {cloudfall-0.2.1 → cloudfall-0.3.0}/examples/render-demo/uv.lock +0 -0
  229. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/README.md +0 -0
  230. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/__init__.py +0 -0
  231. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/__main__.py +0 -0
  232. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/agent_tools.py +0 -0
  233. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/arguments.py +0 -0
  234. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/audit.py +0 -0
  235. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/authoring.py +0 -0
  236. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/commands.py +0 -0
  237. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/cutover.py +0 -0
  238. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/dashboard.py +0 -0
  239. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/dashboard_server.py +0 -0
  240. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/domain.py +0 -0
  241. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/importer.py +0 -0
  242. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/inventory.py +0 -0
  243. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/lifecycle.py +0 -0
  244. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/mcp_server.py +0 -0
  245. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/migrate.py +0 -0
  246. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/observation.py +0 -0
  247. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/operations.py +0 -0
  248. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/operator.py +0 -0
  249. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/py.typed +0 -0
  250. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/render_api.py +0 -0
  251. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/resources.py +0 -0
  252. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/secrets.py +0 -0
  253. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/service_evidence.py +0 -0
  254. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/src/cloudfall/validation.py +0 -0
  255. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_arguments.py +0 -0
  256. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_audit.py +0 -0
  257. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_commands.py +0 -0
  258. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_cutover.py +0 -0
  259. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_dashboard_server.py +0 -0
  260. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_data_migration.py +0 -0
  261. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_importer.py +0 -0
  262. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_inventory.py +0 -0
  263. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_lifecycle.py +0 -0
  264. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_migrate.py +0 -0
  265. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_operations_dashboard.py +0 -0
  266. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_operator.py +0 -0
  267. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_render_api.py +0 -0
  268. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_resources.py +0 -0
  269. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_secrets.py +0 -0
  270. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_service_evidence.py +0 -0
  271. {cloudfall-0.2.1 → cloudfall-0.3.0}/sdk/tests/test_validation.py +0 -0
  272. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/public/apple-touch-icon.png +0 -0
  273. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/public/favicon-32.png +0 -0
  274. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/public/favicon.svg +0 -0
  275. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/public/icon-192.png +0 -0
  276. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/public/icon-512.png +0 -0
  277. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/public/index.html +0 -0
  278. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/public/manifesto.html +0 -0
  279. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/public/og.png +0 -0
  280. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/public/site.webmanifest +0 -0
  281. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/src/worker.js +0 -0
  282. {cloudfall-0.2.1 → cloudfall-0.3.0}/site/wrangler.jsonc +0 -0
@@ -6,6 +6,25 @@ 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
+
9
28
  ## [0.2.1] — 2026-09-20
10
29
 
11
30
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cloudfall
3
- Version: 0.2.1
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
@@ -198,10 +198,8 @@ Your servers, applications, evidence, and any playbooks of your own belong in
198
198
  a project: a directory of its own, kept private. Cloudfall's wheel bundles the
199
199
  schema catalog and the Ansible engine, so the project needs no checkout of
200
200
  this repository. `cloudfall init` is the first command, and it pins the
201
- project to the Cloudfall that ran it: the released version when that came
202
- from PyPI, the exact commit when it came from git or a clone. Without a
203
- directory argument it initializes the current directory, which must be
204
- empty:
201
+ project to the released Cloudfall that ran it. Without a directory argument
202
+ it initializes the current directory, which must be empty:
205
203
 
206
204
  ```console
207
205
  uvx cloudfall init my-project
@@ -230,10 +228,11 @@ dependencies = ["cloudfall==<version>"]
230
228
  package = false
231
229
  ```
232
230
 
233
- Bump that pin and run `uv sync` to move the project to a newer Cloudfall. A
234
- project initialized from a git or source install pins a commit instead, with
235
- a `[tool.uv.sources]` table naming the repository and revision; `--rev` asks
236
- for that explicitly.
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.
237
236
 
238
237
  Only the kind directories are read as resources, so playbooks, roles, docs,
239
238
  and tooling files may live anywhere else in the project. Commands find the
@@ -172,10 +172,8 @@ Your servers, applications, evidence, and any playbooks of your own belong in
172
172
  a project: a directory of its own, kept private. Cloudfall's wheel bundles the
173
173
  schema catalog and the Ansible engine, so the project needs no checkout of
174
174
  this repository. `cloudfall init` is the first command, and it pins the
175
- project to the Cloudfall that ran it: the released version when that came
176
- from PyPI, the exact commit when it came from git or a clone. Without a
177
- directory argument it initializes the current directory, which must be
178
- empty:
175
+ project to the released Cloudfall that ran it. Without a directory argument
176
+ it initializes the current directory, which must be empty:
179
177
 
180
178
  ```console
181
179
  uvx cloudfall init my-project
@@ -204,10 +202,11 @@ dependencies = ["cloudfall==<version>"]
204
202
  package = false
205
203
  ```
206
204
 
207
- Bump that pin and run `uv sync` to move the project to a newer Cloudfall. A
208
- project initialized from a git or source install pins a commit instead, with
209
- a `[tool.uv.sources]` table naming the repository and revision; `--rev` asks
210
- for that explicitly.
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.
211
210
 
212
211
  Only the kind directories are read as resources, so playbooks, roles, docs,
213
212
  and tooling files may live anywhere else in the project. Commands find the
@@ -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.1"
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,18 +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
- GitPin,
102
- GitRevision,
103
- GitSourceUrl,
104
100
  InitOptions,
105
101
  ProjectDescription,
106
102
  ProjectError,
107
103
  ProjectName,
108
104
  init_project,
109
105
  project_context,
110
- resolve_installed_pin,
106
+ resolve_installed_version,
111
107
  )
112
108
  from cloudfall.render_api import (
113
109
  HttpRenderApiClient,
@@ -200,14 +196,6 @@ def _add_init_parser(
200
196
  "--name",
201
197
  help="package name written to pyproject.toml (default: the directory name)",
202
198
  )
203
- init_parser.add_argument(
204
- "--rev",
205
- help=(
206
- "Cloudfall commit to pin, installed from --source (default: the "
207
- "released version of the running cloudfall, or the commit it was "
208
- "installed from when that was a git or source install)"
209
- ),
210
- )
211
199
  init_parser.add_argument(
212
200
  "--description",
213
201
  help=(
@@ -215,14 +203,6 @@ def _add_init_parser(
215
203
  "and pyproject.toml"
216
204
  ),
217
205
  )
218
- init_parser.add_argument(
219
- "--source",
220
- default=DEFAULT_SOURCE_URL,
221
- help=(
222
- "git location of Cloudfall to install from, with --rev "
223
- f"(default: {DEFAULT_SOURCE_URL})"
224
- ),
225
- )
226
206
 
227
207
 
228
208
  def _add_add_parsers(
@@ -1033,18 +1013,10 @@ def _run_init(arguments: Namespace) -> int:
1033
1013
  if arguments.name is not None
1034
1014
  else ProjectName.from_directory(directory)
1035
1015
  )
1036
- pin = (
1037
- GitPin(
1038
- GitRevision.from_boundary(arguments.rev),
1039
- GitSourceUrl.from_boundary(arguments.source),
1040
- )
1041
- if arguments.rev is not None
1042
- else resolve_installed_pin()
1043
- )
1044
1016
  options = InitOptions(
1045
1017
  directory=directory,
1046
1018
  name=name,
1047
- pin=pin,
1019
+ version=resolve_installed_version(),
1048
1020
  description=(
1049
1021
  ProjectDescription.from_boundary(arguments.description)
1050
1022
  if arguments.description is not None
@@ -4,8 +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 the project runs with: a
8
- released version from the package index, or a commit in the repository.
7
+ ``pyproject.toml`` that pins the released Cloudfall the project runs with.
9
8
  Every ``cloudfall`` command runs inside a project: the current directory
10
9
  when it is one, else ``--project``, else ``CLOUDFALL_PROJECT``.
11
10
  Evidence, receipts, and rendered files land under ``tmp/`` inside the
@@ -14,7 +13,6 @@ project and are never committed.
14
13
 
15
14
  from __future__ import annotations
16
15
 
17
- import json
18
16
  import os
19
17
  import re
20
18
  import shutil
@@ -26,7 +24,6 @@ from enum import Enum
26
24
  from importlib import metadata
27
25
  from pathlib import Path
28
26
  from typing import TYPE_CHECKING
29
- from urllib.parse import urlparse
30
27
 
31
28
  from cloudfall.commands import CommandEffect, commands_with_effect
32
29
  from cloudfall.domain import ResourceKind
@@ -34,20 +31,17 @@ from cloudfall.domain import ResourceKind
34
31
  if TYPE_CHECKING:
35
32
  from collections.abc import Callable, Iterator, Mapping
36
33
 
37
- _GIT_REVISION_PATTERN = re.compile(r"^[0-9a-f]{40}$")
38
34
  _PROJECT_NAME_PATTERN = re.compile(r"^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$")
39
35
  _RELEASE_VERSION_PATTERN = re.compile(
40
36
  r"^\d+(?:\.\d+)*(?:(?:a|b|rc)\d+)?(?:\.post\d+)?(?:\.dev\d+)?$"
41
37
  )
42
- _SCP_LIKE_SOURCE_PATTERN = re.compile(
43
- r"^(?P<user>[\w.-]+)@(?P<host>[\w.-]+):(?P<path>[\w./-]+)$"
44
- )
45
38
  _PROJECT_NAME_MAX_LENGTH = 64
46
39
  _PROJECT_DESCRIPTION_MAX_LENGTH = 512
47
40
  _GIT_TIMEOUT_SECONDS = 30
48
41
  _GIT_NOT_A_REPOSITORY_EXIT_CODE = 128
49
42
 
50
- 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."""
51
45
 
52
46
  SECRETS_DIRECTORY = "secrets"
53
47
  """Project directory holding sops-encrypted secret fragments."""
@@ -70,9 +64,6 @@ CLAUDE_POINTER_FILE = "CLAUDE.md"
70
64
  _ERROR_NAME_UNDERIVED = "project_name_underived"
71
65
  _ERROR_DIRECTORY_MISSING = "project_directory_missing"
72
66
  _ERROR_DIRECTORY_UNRESOLVED = "project_directory_unresolved"
73
- _ERROR_REVISION_UNRESOLVED = "project_revision_unresolved"
74
- _ERROR_REVISION_UNCOMMITTED = "project_revision_uncommitted"
75
- _ERROR_REVISION_UNPUBLISHED = "project_revision_unpublished"
76
67
  _ERROR_VERSION_UNRESOLVED = "project_version_unresolved"
77
68
  _ERROR_NOT_A_DIRECTORY = "project_directory_not_a_directory"
78
69
  _ERROR_NOT_EMPTY = "project_directory_not_empty"
@@ -104,76 +95,6 @@ class ProjectError(RuntimeError):
104
95
  }
105
96
 
106
97
 
107
- @dataclass(frozen=True, slots=True)
108
- class GitRevision:
109
- """Full 40-character commit hash a project pins Cloudfall to."""
110
-
111
- value: str
112
-
113
- def __post_init__(self) -> None:
114
- """Enforce the full-hash invariant at construction time."""
115
- if not _GIT_REVISION_PATTERN.fullmatch(self.value):
116
- message = f"git revision must be a full 40-hex commit hash: {self.value!r}"
117
- raise ValueError(message)
118
-
119
- @classmethod
120
- def from_boundary(cls, value: object) -> GitRevision:
121
- """Coerce a boundary value while keeping internal APIs strictly typed."""
122
- if not isinstance(value, str):
123
- message = f"git revision must be a string, got {type(value).__name__}"
124
- raise TypeError(message)
125
- return cls(value.strip().lower())
126
-
127
- def __str__(self) -> str:
128
- """Return the serialized hash."""
129
- return self.value
130
-
131
-
132
- @dataclass(frozen=True, slots=True)
133
- class GitSourceUrl:
134
- """Location of the Cloudfall repository a project installs from."""
135
-
136
- value: str
137
-
138
- def __post_init__(self) -> None:
139
- """Accept https and ssh git locations only."""
140
- parsed = urlparse(self.value)
141
- https = parsed.scheme == "https" and bool(parsed.netloc)
142
- ssh = parsed.scheme == "ssh" and bool(parsed.netloc)
143
- scp_like = _SCP_LIKE_SOURCE_PATTERN.fullmatch(self.value) is not None
144
- if not (https or ssh or scp_like):
145
- message = f"git source must be an https or ssh location: {self.value!r}"
146
- raise ValueError(message)
147
-
148
- @classmethod
149
- def from_boundary(cls, value: object) -> GitSourceUrl:
150
- """Coerce a boundary value while keeping internal APIs strictly typed."""
151
- if not isinstance(value, str):
152
- message = f"git source must be a string, got {type(value).__name__}"
153
- raise TypeError(message)
154
- return cls(value.strip())
155
-
156
- @property
157
- def browse_url(self) -> str:
158
- """Return the https location of the repository for links to its files.
159
-
160
- ``https://host/owner/repo.git``, ``ssh://git@host/owner/repo.git``, and
161
- ``git@host:owner/repo.git`` all browse at ``https://host/owner/repo``.
162
- """
163
- scp_like = _SCP_LIKE_SOURCE_PATTERN.fullmatch(self.value)
164
- if scp_like is not None:
165
- host, path = scp_like.group("host"), scp_like.group("path")
166
- else:
167
- parsed = urlparse(self.value)
168
- host, path = parsed.hostname or "", parsed.path
169
- path = path.strip("/").removesuffix(".git")
170
- return f"https://{host}/{path}"
171
-
172
- def __str__(self) -> str:
173
- """Return the serialized location."""
174
- return self.value
175
-
176
-
177
98
  @dataclass(frozen=True, slots=True)
178
99
  class ReleaseVersion:
179
100
  """Released Cloudfall version a project pins from the package index."""
@@ -199,84 +120,6 @@ class ReleaseVersion:
199
120
  return self.value
200
121
 
201
122
 
202
- @dataclass(frozen=True, slots=True)
203
- class GitPin:
204
- """A project that installs Cloudfall from a commit in the repository."""
205
-
206
- revision: GitRevision
207
- source: GitSourceUrl = GitSourceUrl(DEFAULT_SOURCE_URL)
208
-
209
- @property
210
- def requirement(self) -> str:
211
- """Return the dependency specifier for the project's ``pyproject``."""
212
- return "cloudfall"
213
-
214
- @property
215
- def sources_table(self) -> str:
216
- """Return the ``[tool.uv.sources]`` table that resolves the commit."""
217
- return f"""
218
- # Cloudfall is an installed package: the schema catalog and the Ansible engine
219
- # ship inside the wheel. Bump `rev` to move this project to a newer Cloudfall
220
- # commit, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
221
- # the `cloudfall-mcp` agent server.
222
- [tool.uv.sources]
223
- cloudfall = {{ git = "{self.source}", rev = "{self.revision}" }}
224
- """
225
-
226
- @property
227
- def browse_url(self) -> str:
228
- """Return the https location of the repository the pin reads from."""
229
- return self.source.browse_url
230
-
231
- @property
232
- def browse_ref(self) -> str:
233
- """Return the git ref whose files match the pinned Cloudfall."""
234
- return str(self.revision)
235
-
236
- def as_dict(self) -> dict[str, object]:
237
- """Serialize the pin for system boundaries."""
238
- return {
239
- "kind": "git",
240
- "revision": str(self.revision),
241
- "source": str(self.source),
242
- }
243
-
244
-
245
- @dataclass(frozen=True, slots=True)
246
- class IndexPin:
247
- """A project that installs a released Cloudfall from the package index."""
248
-
249
- version: ReleaseVersion
250
-
251
- @property
252
- def requirement(self) -> str:
253
- """Return the dependency specifier for the project's ``pyproject``."""
254
- return f"cloudfall=={self.version}"
255
-
256
- @property
257
- def sources_table(self) -> str:
258
- """Return nothing: a released version needs no source override."""
259
- return ""
260
-
261
- @property
262
- def browse_url(self) -> str:
263
- """Return the https location of the repository the release came from."""
264
- return GitSourceUrl(DEFAULT_SOURCE_URL).browse_url
265
-
266
- @property
267
- def browse_ref(self) -> str:
268
- """Return the release tag whose files match the pinned Cloudfall."""
269
- return f"v{self.version}"
270
-
271
- def as_dict(self) -> dict[str, object]:
272
- """Serialize the pin for system boundaries."""
273
- return {"kind": "index", "version": str(self.version)}
274
-
275
-
276
- type ProjectPin = GitPin | IndexPin
277
- """What a project pins Cloudfall to: a released version, or a commit."""
278
-
279
-
280
123
  @dataclass(frozen=True, slots=True)
281
124
  class ProjectName:
282
125
  """Package name of a project, as written into its ``pyproject.toml``."""
@@ -363,7 +206,7 @@ class InitOptions:
363
206
 
364
207
  directory: Path
365
208
  name: ProjectName
366
- pin: ProjectPin
209
+ version: ReleaseVersion
367
210
  description: ProjectDescription | None = None
368
211
 
369
212
 
@@ -383,7 +226,7 @@ class ProjectScaffold:
383
226
 
384
227
  directory: Path
385
228
  name: ProjectName
386
- pin: ProjectPin
229
+ version: ReleaseVersion
387
230
  files: tuple[str, ...]
388
231
  git: GitSetup
389
232
 
@@ -394,7 +237,7 @@ class ProjectScaffold:
394
237
  "project": {
395
238
  "directory": str(self.directory),
396
239
  "name": str(self.name),
397
- "pin": self.pin.as_dict(),
240
+ "version": str(self.version),
398
241
  "git": self.git.value,
399
242
  },
400
243
  "files": list(self.files),
@@ -407,17 +250,6 @@ class ProjectScaffold:
407
250
  }
408
251
 
409
252
 
410
- @dataclass(frozen=True, slots=True)
411
- class CheckoutState:
412
- """What a source checkout says about the commit it is running."""
413
-
414
- head: str
415
- clean: bool
416
- """No tracked file differs from ``HEAD``, so ``HEAD`` is the running code."""
417
- published: bool
418
- """``HEAD`` is reachable from a remote-tracking branch, so it can be fetched."""
419
-
420
-
421
253
  def is_project(directory: Path) -> bool:
422
254
  """Return whether a directory holds at least one resource kind directory."""
423
255
  return directory.is_dir() and any(
@@ -493,77 +325,24 @@ def project_path(value: str) -> Path:
493
325
  return path
494
326
 
495
327
 
496
- def resolve_installed_pin(
497
- read_direct_url: Callable[[], str | None] | None = None,
498
- inspect_checkout: Callable[[Path], CheckoutState] | None = None,
499
- read_version: Callable[[], str] | None = None,
500
- ) -> ProjectPin:
501
- """Return what the running Cloudfall pins a new project to.
502
-
503
- An install from the package index records no origin, and its version is
504
- the pin: the project depends on ``cloudfall==<version>`` and resolves it
505
- the ordinary way. A wheel installed from git records its commit in
506
- ``direct_url.json``; an editable source checkout records the checkout
507
- location, whose ``HEAD`` is the commit, provided the checkout is clean
508
- (else ``HEAD`` is not the code that is running) and ``HEAD`` is on a
509
- remote branch (else ``uv sync`` cannot fetch it). Anything else cannot be
510
- pinned and must be given explicitly.
511
- """
512
- direct_url = (read_direct_url or _installed_direct_url)()
513
- if direct_url is None:
514
- return IndexPin(_installed_release_version(read_version))
515
- record = json.loads(direct_url)
516
- if not isinstance(record, dict):
517
- message = f"direct_url.json is not an object: {direct_url!r}"
518
- raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
519
- vcs_info = record.get("vcs_info")
520
- if isinstance(vcs_info, dict) and "commit_id" in vcs_info:
521
- return GitPin(GitRevision.from_boundary(vcs_info["commit_id"]))
522
- dir_info = record.get("dir_info")
523
- url = record.get("url")
524
- if (
525
- isinstance(dir_info, dict)
526
- and isinstance(url, str)
527
- and url.startswith("file://")
528
- ):
529
- checkout = Path(urlparse(url).path)
530
- inspect = inspect_checkout or _inspect_checkout
531
- return GitPin(_pinnable_head(checkout, inspect(checkout)))
532
- message = (
533
- "the installed cloudfall distribution is neither a git install nor a "
534
- f"source checkout ({url!r}); pass --rev with the commit to pin"
535
- )
536
- raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
537
-
538
-
539
- def _pinnable_head(checkout: Path, state: CheckoutState) -> GitRevision:
540
- head = GitRevision.from_boundary(state.head)
541
- if not state.clean:
542
- message = (
543
- f"the source checkout {checkout} has uncommitted changes, so HEAD "
544
- f"{head} is not the code that is running; commit them or pass --rev"
545
- )
546
- raise ProjectError(_ERROR_REVISION_UNCOMMITTED, message)
547
- if not state.published:
548
- message = (
549
- f"HEAD {head} of the source checkout {checkout} is on no remote "
550
- "branch, so `uv sync` could not fetch it; push it or pass --rev"
551
- )
552
- raise ProjectError(_ERROR_REVISION_UNPUBLISHED, message)
553
- return head
554
-
555
-
556
- def _installed_release_version(
328
+ def resolve_installed_version(
557
329
  read_version: Callable[[], str] | None = None,
558
330
  ) -> ReleaseVersion:
331
+ """Return the released version of the running Cloudfall.
332
+
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.
337
+ """
559
338
  raw = (read_version or _installed_version)()
560
339
  try:
561
340
  return ReleaseVersion.from_boundary(raw)
562
341
  except ValueError as error:
563
342
  message = (
564
- "the installed cloudfall distribution records no origin and its "
565
- f"version {raw!r} is not a release, so there is nothing to pin; "
566
- "pass --rev with the commit to pin"
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"
567
346
  )
568
347
  raise ProjectError(_ERROR_VERSION_UNRESOLVED, message) from error
569
348
 
@@ -572,10 +351,6 @@ def _installed_version() -> str:
572
351
  return metadata.version("cloudfall")
573
352
 
574
353
 
575
- def _installed_direct_url() -> str | None:
576
- return metadata.distribution("cloudfall").read_text("direct_url.json")
577
-
578
-
579
354
  def _git_binary() -> str:
580
355
  git = shutil.which("git")
581
356
  if git is None:
@@ -609,15 +384,6 @@ def _git_or_fail(directory: Path, *arguments: str) -> str:
609
384
  return completed.stdout
610
385
 
611
386
 
612
- def _inspect_checkout(checkout: Path) -> CheckoutState:
613
- head = _git_or_fail(checkout, "rev-parse", "HEAD").strip()
614
- changes = _git_or_fail(checkout, "status", "--porcelain", "--untracked-files=no")
615
- remote_branches = _git_or_fail(checkout, "branch", "--remotes", "--contains", head)
616
- return CheckoutState(
617
- head=head, clean=not changes.strip(), published=bool(remote_branches.strip())
618
- )
619
-
620
-
621
387
  def _initialize_git(directory: Path) -> GitSetup:
622
388
  """Create a repository in the directory unless one already encloses it."""
623
389
  enclosing = _run_git(directory, "rev-parse", "--is-inside-work-tree")
@@ -666,7 +432,7 @@ def init_project(
666
432
  return ProjectScaffold(
667
433
  directory=directory,
668
434
  name=options.name,
669
- pin=options.pin,
435
+ version=options.version,
670
436
  files=tuple(files),
671
437
  git=git,
672
438
  )
@@ -684,14 +450,14 @@ version = "0"
684
450
  description = "{description}"
685
451
  requires-python = ">=3.14"
686
452
  # Cloudfall is an installed package: the schema catalog and the Ansible engine
687
- # ship inside the wheel. Bump this pin to move the project to a newer
453
+ # ship inside the wheel. Bump this version to move the project to a newer
688
454
  # Cloudfall, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
689
455
  # the `cloudfall-mcp` agent server.
690
- dependencies = ["{options.pin.requirement}"]
456
+ dependencies = ["cloudfall=={options.version}"]
691
457
 
692
458
  [tool.uv]
693
459
  package = false
694
- {options.pin.sources_table}"""
460
+ """
695
461
 
696
462
 
697
463
  _GITIGNORE = f"""# Runtime state: evidence, receipts, rendered inventory, built
@@ -722,8 +488,8 @@ creation_rules:
722
488
  def _readme(options: InitOptions, git: GitSetup) -> str:
723
489
  tmp = RUNTIME_DIRECTORY
724
490
  secrets = SECRETS_DIRECTORY
725
- browse = options.pin.browse_url
726
- ref = options.pin.browse_ref
491
+ browse = REPOSITORY_URL
492
+ ref = f"v{options.version}"
727
493
  secrets_guide = f"{browse}/blob/{ref}/{SECRETS_GUIDE_PATH}"
728
494
  examples = f"{browse}/tree/{ref}/{EXAMPLES_PATH}"
729
495
  about = (
@@ -808,9 +574,9 @@ uv run cloudfall migrate --build <component>=main
808
574
  uv run cloudfall migrate --build <component>=main --yes
809
575
  ```
810
576
 
811
- Cloudfall is pinned to one commit in `pyproject.toml`; bump `rev` and run
812
- `uv sync` to move the project to a newer release. The links below point at
813
- 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.
814
580
 
815
581
  [cloudfall]: {browse}
816
582
  [secrets-guide]: {secrets_guide}
@@ -9,10 +9,9 @@ import pytest
9
9
  import yaml
10
10
  from cloudfall.agent_tools import AgentConfig, AgentToolset
11
11
  from cloudfall.project import (
12
- GitPin,
13
- GitRevision,
14
12
  InitOptions,
15
13
  ProjectName,
14
+ ReleaseVersion,
16
15
  init_project,
17
16
  )
18
17
 
@@ -32,7 +31,7 @@ services:
32
31
  """
33
32
 
34
33
 
35
- REVISION = "5dea76ae7fdf4f99f5e8eb84b939dfa68b12ef3b"
34
+ VERSION = "0.3.0"
36
35
 
37
36
 
38
37
  def _config(
@@ -147,7 +146,7 @@ def _fresh_project(tmp_path: Path) -> AgentToolset:
147
146
  InitOptions(
148
147
  directory=directory,
149
148
  name=ProjectName("project"),
150
- pin=GitPin(GitRevision(REVISION)),
149
+ version=ReleaseVersion(VERSION),
151
150
  )
152
151
  )
153
152
  return AgentToolset(_config(tmp_path, project_directory=directory))
@@ -18,10 +18,9 @@ from cloudfall.authoring import (
18
18
  from cloudfall.cli import main
19
19
  from cloudfall.domain import ConnectionAddress, LinuxUser, ResourceId, TcpPort
20
20
  from cloudfall.project import (
21
- GitPin,
22
- GitRevision,
23
21
  InitOptions,
24
22
  ProjectName,
23
+ ReleaseVersion,
25
24
  init_project,
26
25
  )
27
26
  from cloudfall.validation import ConfigValidationError, validate_config
@@ -29,7 +28,7 @@ from cloudfall.validation import ConfigValidationError, validate_config
29
28
  ROOT = Path(__file__).parents[2]
30
29
  SCHEMAS = ROOT / "config" / "schemas" / "v1"
31
30
  EXAMPLES = ROOT / "config" / "examples"
32
- REVISION = "5dea76ae7fdf4f99f5e8eb84b939dfa68b12ef3b"
31
+ VERSION = "0.3.0"
33
32
 
34
33
 
35
34
  def _example_key() -> str:
@@ -52,7 +51,7 @@ def _project(tmp_path: Path) -> Path:
52
51
  InitOptions(
53
52
  directory=directory,
54
53
  name=ProjectName("project"),
55
- pin=GitPin(GitRevision(REVISION)),
54
+ version=ReleaseVersion(VERSION),
56
55
  )
57
56
  )
58
57
  return directory