redeploy 0.2.76__tar.gz → 0.2.77__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 (284) hide show
  1. {redeploy-0.2.76 → redeploy-0.2.77}/PKG-INFO +5 -5
  2. {redeploy-0.2.76 → redeploy-0.2.77}/README.md +4 -4
  3. {redeploy-0.2.76 → redeploy-0.2.77}/pyproject.toml +1 -1
  4. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/plan_apply.py +19 -338
  6. redeploy-0.2.77/redeploy/cli/commands/plan_apply_report.py +296 -0
  7. redeploy-0.2.77/redeploy/cli/commands/plan_apply_run.py +104 -0
  8. redeploy-0.2.77/redeploy/discovery/helpers.py +32 -0
  9. redeploy-0.2.77/redeploy/discovery/probe_parse.py +45 -0
  10. redeploy-0.2.77/redeploy/discovery/registry.py +64 -0
  11. redeploy-0.2.77/redeploy/discovery/scanners.py +254 -0
  12. redeploy-0.2.77/redeploy/discovery/ssh_credentials.py +111 -0
  13. redeploy-0.2.77/redeploy/discovery/types.py +38 -0
  14. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/discovery.py +3 -99
  15. redeploy-0.2.77/redeploy/discovery_probe.py +45 -0
  16. redeploy-0.2.77/redeploy/discovery_registry.py +66 -0
  17. redeploy-0.2.77/redeploy/tests/test_discovery_probe.py +45 -0
  18. redeploy-0.2.77/redeploy/tests/test_plan_apply_report.py +91 -0
  19. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy.egg-info/PKG-INFO +5 -5
  20. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy.egg-info/SOURCES.txt +12 -0
  21. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/__init__.py +0 -0
  22. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/checkers/__init__.py +0 -0
  23. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/checkers/base.py +0 -0
  24. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/checkers/binary.py +0 -0
  25. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/checkers/command_ref.py +0 -0
  26. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/checkers/compose.py +0 -0
  27. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/checkers/docker_build.py +0 -0
  28. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/checkers/env.py +0 -0
  29. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/checkers/path.py +0 -0
  30. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/checkers/reference.py +0 -0
  31. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/ignore.py +0 -0
  32. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/models.py +0 -0
  33. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/preflight_schema.py +0 -0
  34. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/analyze/spec_analyzer.py +0 -0
  35. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/__init__.py +0 -0
  36. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/exceptions.py +0 -0
  37. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/executor.py +0 -0
  38. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/handlers.py +0 -0
  39. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/progress.py +0 -0
  40. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/rollback.py +0 -0
  41. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/state.py +0 -0
  42. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/state_apply.py +0 -0
  43. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/utils/__init__.py +0 -0
  44. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/apply/utils/run_container_build.py +0 -0
  45. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/audit/__init__.py +0 -0
  46. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/audit/auditor.py +0 -0
  47. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/audit/extractor.py +0 -0
  48. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/audit/models.py +0 -0
  49. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/audit/paths.py +0 -0
  50. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/audit/patterns.py +0 -0
  51. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/audit/probe.py +0 -0
  52. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/__init__.py +0 -0
  53. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/extractor.py +0 -0
  54. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/generators/__init__.py +0 -0
  55. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/generators/docker_compose.py +0 -0
  56. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/generators/migration.py +0 -0
  57. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/sources/__init__.py +0 -0
  58. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/sources/compose.py +0 -0
  59. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/sources/hardware.py +0 -0
  60. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/sources/infra.py +0 -0
  61. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/blueprint/sources/migration.py +0 -0
  62. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/__init__.py +0 -0
  63. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/__init__.py +0 -0
  64. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/audit.py +0 -0
  65. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/blueprint.py +0 -0
  66. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/bump_fix.py +0 -0
  67. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/detect.py +0 -0
  68. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/device_map.py +0 -0
  69. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/device_map_renderers.py +0 -0
  70. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/devices.py +0 -0
  71. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/diagnose.py +0 -0
  72. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/diff.py +0 -0
  73. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/exec_.py +0 -0
  74. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/export.py +0 -0
  75. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/gh_workflow.py +0 -0
  76. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/hardware.py +0 -0
  77. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/import_.py +0 -0
  78. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/init.py +0 -0
  79. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/inspect.py +0 -0
  80. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/lint.py +0 -0
  81. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/mcp_cmd.py +0 -0
  82. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/patterns.py +0 -0
  83. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/plugin.py +0 -0
  84. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/probe.py +0 -0
  85. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/prompt_cmd.py +0 -0
  86. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/push.py +0 -0
  87. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/state.py +0 -0
  88. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/status.py +0 -0
  89. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/target.py +0 -0
  90. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/version/__init__.py +0 -0
  91. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/version/commands.py +0 -0
  92. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/version/helpers.py +0 -0
  93. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/version/monorepo.py +0 -0
  94. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/version/release.py +0 -0
  95. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/version/scanner.py +0 -0
  96. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/version/utils/__init__.py +0 -0
  97. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
  98. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/version/utils/git_config.py +0 -0
  99. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/commands/workflow.py +0 -0
  100. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/core.py +0 -0
  101. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/display.py +0 -0
  102. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli/query.py +0 -0
  103. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/cli.py +0 -0
  104. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/config_apply/__init__.py +0 -0
  105. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/config_apply/applier.py +0 -0
  106. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/config_apply/handlers/__init__.py +0 -0
  107. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/config_apply/handlers/display.py +0 -0
  108. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/config_apply/loader.py +0 -0
  109. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/data_sync.py +0 -0
  110. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/detect/__init__.py +0 -0
  111. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/detect/builtin/__init__.py +0 -0
  112. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/detect/detector.py +0 -0
  113. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/detect/hardware.py +0 -0
  114. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/detect/hardware_rules.py +0 -0
  115. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/detect/probes.py +0 -0
  116. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/detect/remote.py +0 -0
  117. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/detect/templates.py +0 -0
  118. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/detect/workflow.py +0 -0
  119. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl/__init__.py +0 -0
  120. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl/loader.py +0 -0
  121. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl/parser.py +0 -0
  122. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl_python/__init__.py +0 -0
  123. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl_python/context.py +0 -0
  124. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl_python/decorators.py +0 -0
  125. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl_python/docker_steps.py +0 -0
  126. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl_python/exceptions.py +0 -0
  127. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl_python/runner.py +0 -0
  128. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/dsl_python/steps.py +0 -0
  129. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/fleet.py +0 -0
  130. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/__init__.py +0 -0
  131. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/config_txt.py +0 -0
  132. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/data/__init__.py +0 -0
  133. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/data/hyperpixel.py +0 -0
  134. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/data/official.py +0 -0
  135. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/data/waveshare.py +0 -0
  136. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/fixes.py +0 -0
  137. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/kiosk/__init__.py +0 -0
  138. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/kiosk/autostart.py +0 -0
  139. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/kiosk/browsers.py +0 -0
  140. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/kiosk/compositors.py +0 -0
  141. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  142. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/panels.py +0 -0
  143. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/hardware/raspi_config.py +0 -0
  144. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/heal/__init__.py +0 -0
  145. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/heal/decider.py +0 -0
  146. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/heal/hint_provider.py +0 -0
  147. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/heal/log_writer.py +0 -0
  148. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/heal/loop_detector.py +0 -0
  149. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/heal/runner.py +0 -0
  150. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/heal.py +0 -0
  151. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/iac/__init__.py +0 -0
  152. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/iac/base.py +0 -0
  153. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/iac/config_hints.py +0 -0
  154. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/iac/docker_compose.py +0 -0
  155. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/iac/parsers/__init__.py +0 -0
  156. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/iac/parsers/compose.py +0 -0
  157. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/iac/registry.py +0 -0
  158. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/integrations/__init__.py +0 -0
  159. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/integrations/op3_bridge.py +0 -0
  160. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/markpact/__init__.py +0 -0
  161. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/markpact/compiler.py +0 -0
  162. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/markpact/models.py +0 -0
  163. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/markpact/parser.py +0 -0
  164. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/mcp_server.py +0 -0
  165. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/__init__.py +0 -0
  166. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/blueprint.py +0 -0
  167. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/devices.py +0 -0
  168. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/enums.py +0 -0
  169. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/hardware.py +0 -0
  170. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/infra.py +0 -0
  171. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/manifest.py +0 -0
  172. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/persisted.py +0 -0
  173. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/pipeline.py +0 -0
  174. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/plan.py +0 -0
  175. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/models/spec.py +0 -0
  176. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/observe.py +0 -0
  177. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/parse.py +0 -0
  178. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/patterns.py +0 -0
  179. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/plan/__init__.py +0 -0
  180. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/plan/planner.py +0 -0
  181. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/plugins/__init__.py +0 -0
  182. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/plugins/builtin/__init__.py +0 -0
  183. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/plugins/builtin/browser_reload.py +0 -0
  184. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  185. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/plugins/builtin/notify.py +0 -0
  186. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/plugins/builtin/process_control.py +0 -0
  187. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  188. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/schema.py +0 -0
  189. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/spec_loader.py +0 -0
  190. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/ssh.py +0 -0
  191. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/__init__.py +0 -0
  192. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/docker.py +0 -0
  193. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/generic.py +0 -0
  194. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/hardware.py +0 -0
  195. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/k3s.py +0 -0
  196. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/kiosk.py +0 -0
  197. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/podman.py +0 -0
  198. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/process.py +0 -0
  199. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/scm.py +0 -0
  200. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/steps/transfer.py +0 -0
  201. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/__init__.py +0 -0
  202. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_analyze.py +0 -0
  203. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_audit.py +0 -0
  204. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_cli.py +0 -0
  205. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_data_sync.py +0 -0
  206. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_discovery.py +0 -0
  207. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_dsl.py +0 -0
  208. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_examples.py +0 -0
  209. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_executor.py +0 -0
  210. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_fleet.py +0 -0
  211. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_git_integration.py +0 -0
  212. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_heal.py +0 -0
  213. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_heal_decider.py +0 -0
  214. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_hooks.py +0 -0
  215. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_iac.py +0 -0
  216. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_iac_compose_contract.py +0 -0
  217. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
  218. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_manifest.py +0 -0
  219. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_markpact_compiler.py +0 -0
  220. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_markpact_parser.py +0 -0
  221. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_models.py +0 -0
  222. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_observe.py +0 -0
  223. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_parse.py +0 -0
  224. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_patterns.py +0 -0
  225. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_planner_edge.py +0 -0
  226. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_plugins.py +0 -0
  227. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_probes.py +0 -0
  228. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_public_api.py +0 -0
  229. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_resume.py +0 -0
  230. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_security_hardening.py +0 -0
  231. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_spec_loader.py +0 -0
  232. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_ssh.py +0 -0
  233. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_steps_registry.py +0 -0
  234. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_templates.py +0 -0
  235. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_verify.py +0 -0
  236. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_version.py +0 -0
  237. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_version_changelog.py +0 -0
  238. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_version_cli.py +0 -0
  239. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_version_manifest.py +0 -0
  240. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_version_sources.py +0 -0
  241. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/tests/test_workflow.py +0 -0
  242. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/verify.py +0 -0
  243. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/__init__.py +0 -0
  244. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/bump.py +0 -0
  245. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/changelog.py +0 -0
  246. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/commits.py +0 -0
  247. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/diff.py +0 -0
  248. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/git_integration.py +0 -0
  249. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/git_transaction.py +0 -0
  250. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/manifest.py +0 -0
  251. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/sources/__init__.py +0 -0
  252. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/sources/base.py +0 -0
  253. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/sources/json_.py +0 -0
  254. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/sources/plain.py +0 -0
  255. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/sources/regex.py +0 -0
  256. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/sources/toml_.py +0 -0
  257. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/sources/yaml_.py +0 -0
  258. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version/transaction.py +0 -0
  259. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy/version.py +0 -0
  260. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy.egg-info/dependency_links.txt +0 -0
  261. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy.egg-info/entry_points.txt +0 -0
  262. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy.egg-info/requires.txt +0 -0
  263. {redeploy-0.2.76 → redeploy-0.2.77}/redeploy.egg-info/top_level.txt +0 -0
  264. {redeploy-0.2.76 → redeploy-0.2.77}/setup.cfg +0 -0
  265. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_blueprint_sources.py +0 -0
  266. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_blueprint_sources_compose.py +0 -0
  267. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_cli_gh_workflow.py +0 -0
  268. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_cli_query.py +0 -0
  269. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_example_argocd_flux_plugin.py +0 -0
  270. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_example_gitops_ci_plugin.py +0 -0
  271. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_example_helm_kustomize_plugin.py +0 -0
  272. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_hardware_config.py +0 -0
  273. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_hardware_kiosk.py +0 -0
  274. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_hardware_rules.py +0 -0
  275. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_heal_decider.py +0 -0
  276. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_heal_loop_detector.py +0 -0
  277. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_iac.py +0 -0
  278. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_iac_config_hints.py +0 -0
  279. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_iac_plugin_loading.py +0 -0
  280. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_import_plugin_template.py +0 -0
  281. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_parse.py +0 -0
  282. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_prompt_llm.py +0 -0
  283. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_redeploy.py +0 -0
  284. {redeploy-0.2.76 → redeploy-0.2.77}/tests/test_steps_kiosk.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.76
3
+ Version: 0.2.77
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -33,17 +33,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
33
33
 
34
34
  ## AI Cost Tracking
35
35
 
36
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.76-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.41-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
36
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.77-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.92-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
38
38
 
39
- - 🤖 **LLM usage:** $5.4083 (98 commits)
39
+ - 🤖 **LLM usage:** $5.9206 (99 commits)
40
40
  - 👤 **Human dev:** ~$4198 (42.0h @ $100/h, 30min dedup)
41
41
 
42
42
  Generated on 2026-05-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
43
43
 
44
44
  ---
45
45
 
46
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.76-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.77-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
47
47
 
48
48
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
49
49
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.76-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.41-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.77-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.92-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-42.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $5.4083 (98 commits)
9
+ - 🤖 **LLM usage:** $5.9206 (99 commits)
10
10
  - 👤 **Human dev:** ~$4198 (42.0h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-05-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.76-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.77-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.76"
7
+ version = "0.2.77"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
7
7
 
8
8
  Everything not listed in ``__all__`` is internal and may change without notice.
9
9
  """
10
- __version__ = "0.2.76"
10
+ __version__ = "0.2.77"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -1,7 +1,6 @@
1
1
  """plan, apply, migrate, run commands — Migration planning and execution."""
2
2
  from __future__ import annotations
3
3
 
4
- import subprocess
5
4
  import sys
6
5
  from datetime import datetime, timezone
7
6
  from pathlib import Path
@@ -20,6 +19,8 @@ from ..core import (
20
19
  overlay_device_onto_spec,
21
20
  )
22
21
  from ..display import print_plan_table
22
+ from .plan_apply_report import write_markdown_report
23
+ from .plan_apply_run import run_lint_phase, run_preflight_phase, setup_run_logging
23
24
 
24
25
 
25
26
  @click.command()
@@ -325,17 +326,13 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
325
326
  from ...models import ProjectManifest
326
327
  from ...plan import Planner
327
328
  from ...plugins import load_user_plugins
328
- from ...apply import Executor
329
329
 
330
330
  console = Console()
331
331
 
332
- # Create deployment ignore policy on first run (project-aware template).
333
332
  _ensure_redeployignore(Path.cwd(), console)
334
333
 
335
- # Load project manifest (redeploy.yaml) if present
336
334
  manifest = ProjectManifest.find_and_load(Path.cwd())
337
335
 
338
- # Resolve spec file: arg > manifest.spec > "migration.yaml"
339
336
  resolved_spec = spec_file or (manifest.spec if manifest else "migration.yaml")
340
337
  if not Path(resolved_spec).exists():
341
338
  console.print(f"[red]✗ spec file not found: {resolved_spec}[/red]")
@@ -343,47 +340,12 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
343
340
  sys.exit(1)
344
341
 
345
342
  spec = load_spec_or_exit(console, resolved_spec)
346
- started_at = datetime.now(timezone.utc)
347
-
348
- # Redirect all logger output to a dated file under .redeploy/logs/
349
- log_dir = Path.cwd() / ".redeploy" / "logs"
350
- log_dir.mkdir(parents=True, exist_ok=True)
351
- log_file = log_dir / f"redeploy-{started_at.strftime('%Y%m%d_%H%M%S')}.log"
352
- file_handler_id = logger.add(
353
- log_file,
354
- level="DEBUG",
355
- format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level:<8} | {name}:{function}:{line} | {message}",
356
- enqueue=True,
357
- )
358
- logger.info("redeploy run started — spec={}", resolved_spec)
343
+ file_handler_id, _log_file, started_at = setup_run_logging(resolved_spec)
359
344
 
360
- # Overlay manifest values
361
345
  _apply_manifest_to_spec(console, manifest, spec, env_name)
362
346
  _print_spec_summary(console, spec)
363
347
 
364
- # Static analysis (lint)
365
- lint_result = None
366
- if lint:
367
- from ...analyze import SpecAnalyzer, IssueSeverity
368
- analyzer = SpecAnalyzer(base_dir=Path.cwd())
369
- _, lint_result = analyzer.analyze_file(Path(resolved_spec))
370
- if lint_result.issues:
371
- console.print(f"\n[bold]lint[/bold]")
372
- for i in lint_result.issues:
373
- color = "red" if i.severity == IssueSeverity.ERROR else "yellow"
374
- prefix = f"[{color}]{i.severity.value}[/{color}]"
375
- step = f" ({i.step_id})" if i.step_id else ""
376
- console.print(f" {prefix}{step} {i.category}: {i.message}")
377
- if i.suggestion:
378
- console.print(f" [dim]→ {i.suggestion}[/dim]")
379
- if not lint_result.passed:
380
- console.print("\n[red]✗ lint failed — fix errors above or use --no-lint to skip.[/red]")
381
- logger.remove(file_handler_id)
382
- sys.exit(1)
383
- else:
384
- console.print(f"[yellow]⚠ lint passed with {len(lint_result.warnings())} warning(s)[/yellow]")
385
- else:
386
- console.print("[green] ✓ lint passed[/green]")
348
+ lint_result = run_lint_phase(console, resolved_spec, lint, file_handler_id)
387
349
 
388
350
  # Optional live detect
389
351
  if do_detect:
@@ -405,39 +367,23 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
405
367
 
406
368
  print_plan_table(console, migration)
407
369
 
408
- # Operational preflight schema (resolved refs/paths/deps) + optional strict gate
409
- if preflight:
410
- from ...analyze import generate_preflight_schema, save_preflight_schema
411
-
412
- preflight_result = generate_preflight_schema(
413
- spec_path=Path(resolved_spec),
414
- spec=spec,
415
- migration=migration,
416
- lint_result=lint_result,
417
- base_dir=Path.cwd(),
418
- remote_check=bool(preflight_remote and not dry_run),
419
- )
420
- save_preflight_schema(preflight_result.schema, Path(preflight_schema_out))
421
-
422
- blockers = len(preflight_result.blockers)
423
- console.print(f"\n[bold]preflight[/bold] [dim]schema saved → {preflight_schema_out}[/dim]")
424
- if blockers:
425
- console.print(f"[yellow]⚠ preflight blockers: {blockers}[/yellow]")
426
- for b in preflight_result.blockers[:10]:
427
- sid = f" ({b.get('step_id')})" if b.get("step_id") else ""
428
- console.print(f" [yellow]- {b.get('type')}{sid}: {b.get('message')}[/yellow]")
429
- if blockers > 10:
430
- console.print(f" [dim]... and {blockers - 10} more blockers in schema file[/dim]")
431
- if strict_preflight:
432
- console.print("\n[red]✗ strict preflight failed — aborting before apply[/red]")
433
- logger.remove(file_handler_id)
434
- sys.exit(1)
435
- else:
436
- console.print("[green] ✓ preflight passed (no blockers)[/green]")
370
+ run_preflight_phase(
371
+ console,
372
+ preflight=preflight,
373
+ resolved_spec=resolved_spec,
374
+ spec=spec,
375
+ migration=migration,
376
+ lint_result=lint_result,
377
+ preflight_schema_out=preflight_schema_out,
378
+ preflight_remote=preflight_remote,
379
+ dry_run=dry_run,
380
+ strict_preflight=strict_preflight,
381
+ file_handler_id=file_handler_id,
382
+ )
437
383
 
438
384
  if plan_only:
439
385
  if report:
440
- _write_markdown_report(
386
+ write_markdown_report(
441
387
  console=console,
442
388
  migration=migration,
443
389
  spec_path=Path(resolved_spec),
@@ -487,7 +433,7 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
487
433
  )
488
434
 
489
435
  if report:
490
- _write_markdown_report(
436
+ write_markdown_report(
491
437
  console=console,
492
438
  migration=migration,
493
439
  spec_path=Path(resolved_spec),
@@ -738,268 +684,3 @@ def _inject_project_sync_step(migration, spec, project_root: Path, console) -> N
738
684
  console.print("[dim]plan: injected sync_project_tree (full rsync before apply)[/dim]")
739
685
 
740
686
 
741
- def _default_report_path(spec_path: Path) -> Path:
742
- return spec_path.parent / f".{spec_path.stem}.md"
743
-
744
-
745
- def _resolve_audit_entry(migration, started_at: datetime, ok: bool):
746
- from ...observe import AuditEntry, DeployAuditLog
747
-
748
- log = DeployAuditLog()
749
- entries = log.filter(host=migration.host, app=migration.app, since=started_at)
750
- if entries:
751
- return entries[-1]
752
-
753
- # Fallback if audit record is unavailable for any reason.
754
- done_count = sum(1 for s in migration.steps if str(s.status).endswith("DONE"))
755
- failed_count = sum(1 for s in migration.steps if str(s.status).endswith("FAILED"))
756
- if ok and done_count == 0 and failed_count == 0:
757
- # plan-only fallback: treat all planned steps as successful planning output
758
- done_count = len(migration.steps)
759
-
760
- data = {
761
- "ts": datetime.now(timezone.utc).isoformat(),
762
- "host": migration.host,
763
- "app": migration.app,
764
- "from_strategy": migration.from_strategy.value,
765
- "to_strategy": migration.to_strategy.value,
766
- "ok": ok,
767
- "dry_run": False,
768
- "elapsed_s": 0.0,
769
- "steps_total": len(migration.steps),
770
- "steps_ok": done_count,
771
- "steps_failed": failed_count,
772
- "steps": [
773
- {
774
- "id": s.id,
775
- "action": s.action.value,
776
- "status": str(s.status).split(".")[-1].lower(),
777
- "result": s.result,
778
- "error": s.error,
779
- }
780
- for s in migration.steps
781
- ],
782
- }
783
- return AuditEntry(data)
784
-
785
-
786
- def _step_command_block(step) -> str:
787
- if step.command:
788
- return step.command.strip()
789
- if step.command_ref:
790
- return f"command_ref: {step.command_ref}"
791
- if step.action.value == "rsync":
792
- parts = [f"rsync {step.src or ''} -> {step.dst or ''}"]
793
- if step.excludes:
794
- parts.append("excludes:")
795
- parts.extend(f"- {x}" for x in step.excludes)
796
- return "\n".join(parts)
797
- if step.action.value == "scp":
798
- return f"scp {step.src or ''} -> {step.dst or ''}"
799
- if step.action.value in {"http_check", "version_check"}:
800
- return f"url: {step.url or ''}\nexpect: {step.expect or ''}".strip()
801
- return f"action: {step.action.value}"
802
-
803
-
804
- def _build_checksum_verification(migration, executed: bool) -> dict | None:
805
- """Build post-deploy sync verification with checksum-aware rsync dry-run."""
806
- sync_step = next((s for s in migration.steps if s.id == "sync_project_tree"), None)
807
- if not sync_step:
808
- return None
809
-
810
- src = (sync_step.src or "").strip()
811
- dst = (sync_step.dst or "").strip()
812
- host = (migration.host or "").strip()
813
-
814
- local_root = Path(src.rstrip("/")).expanduser() if src else Path.cwd()
815
- remote_root = dst.rstrip("/") if dst else ""
816
-
817
- if not executed:
818
- return {
819
- "status": "skipped",
820
- "reason": "plan-only run (apply not executed)",
821
- "host": host,
822
- "local_root": str(local_root),
823
- "remote_root": remote_root,
824
- "command": "(not executed)",
825
- "changed_paths": [],
826
- "changed_count": 0,
827
- }
828
-
829
- if not host or host.lower() in {"local", "localhost", "127.0.0.1"}:
830
- return {
831
- "status": "skipped",
832
- "reason": "remote checksum verification requires SSH host",
833
- "host": host,
834
- "local_root": str(local_root),
835
- "remote_root": remote_root,
836
- "command": "(not executed)",
837
- "changed_paths": [],
838
- "changed_count": 0,
839
- }
840
-
841
- if not local_root.exists() or not remote_root:
842
- return {
843
- "status": "error",
844
- "reason": "sync scope is incomplete (missing local or remote root)",
845
- "host": host,
846
- "local_root": str(local_root),
847
- "remote_root": remote_root,
848
- "command": "(not executed)",
849
- "changed_paths": [],
850
- "changed_count": 0,
851
- }
852
-
853
- cmd = ["rsync", "-a", "-z", "--delete", "--checksum", "--dry-run", "--itemize-changes"]
854
- if (local_root / ".gitignore").exists():
855
- cmd += ["--filter=:- .gitignore"]
856
- if (local_root / ".redeployignore").exists():
857
- cmd += ["--filter=:- .redeployignore"]
858
- for ex in sync_step.excludes or []:
859
- cmd += ["--exclude", ex]
860
- cmd += [f"{local_root.as_posix().rstrip('/')}/", f"{host}:{remote_root.rstrip('/')}/"]
861
-
862
- command_preview = " ".join(cmd)
863
- try:
864
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=1200)
865
- except Exception as exc: # pragma: no cover - defensive, non-fatal for report
866
- return {
867
- "status": "error",
868
- "reason": str(exc),
869
- "host": host,
870
- "local_root": str(local_root),
871
- "remote_root": remote_root,
872
- "command": command_preview,
873
- "changed_paths": [],
874
- "changed_count": 0,
875
- }
876
-
877
- raw_lines = [ln.rstrip() for ln in (result.stdout or "").splitlines() if ln.strip()]
878
- changed_lines = [
879
- ln
880
- for ln in raw_lines
881
- if ln not in {"sending incremental file list"}
882
- and not ln.startswith("sent ")
883
- and not ln.startswith("total size is ")
884
- and not ln.startswith("created directory ")
885
- ]
886
-
887
- status = "match" if result.returncode == 0 and not changed_lines else "mismatch"
888
- reason = ""
889
- if result.returncode != 0:
890
- status = "error"
891
- reason = (result.stderr or "").strip() or f"rsync exited with {result.returncode}"
892
-
893
- return {
894
- "status": status,
895
- "reason": reason,
896
- "host": host,
897
- "local_root": str(local_root),
898
- "remote_root": remote_root,
899
- "command": command_preview,
900
- "changed_paths": changed_lines[:200],
901
- "changed_count": len(changed_lines),
902
- }
903
-
904
-
905
- def _render_markdown_report(entry, migration, spec_path: Path, checksum: dict | None = None) -> str:
906
- by_id = {s.id: s for s in migration.steps}
907
- lines: list[str] = [
908
- f"# Redeploy Execution Report - {spec_path.name}",
909
- "",
910
- f"- Timestamp: {entry.ts}",
911
- f"- Spec: {spec_path}",
912
- f"- Host: {entry.host}",
913
- f"- App: {entry.app}",
914
- f"- Strategy: {entry.from_strategy} -> {entry.to_strategy}",
915
- f"- Result: {'SUCCESS' if entry.ok else 'FAILED'}",
916
- f"- Steps: {entry.steps_ok}/{entry.steps_total} ok",
917
- f"- Elapsed: {entry.elapsed_s:.1f}s",
918
- "",
919
- ]
920
-
921
- if checksum:
922
- status = (checksum.get("status") or "unknown").upper()
923
- lines += [
924
- "## Sync Checksum Verification",
925
- "",
926
- "- Method: rsync --checksum --dry-run --itemize-changes",
927
- f"- Scope: {checksum.get('local_root', '')} -> {checksum.get('host', '')}:{checksum.get('remote_root', '')}",
928
- f"- Status: {status}",
929
- f"- Changed Paths: {checksum.get('changed_count', 0)}",
930
- ]
931
- reason = (checksum.get("reason") or "").strip()
932
- if reason:
933
- lines.append(f"- Note: {reason}")
934
- lines += [
935
- "",
936
- "#### Command",
937
- "```bash",
938
- checksum.get("command") or "(not executed)",
939
- "```",
940
- "",
941
- ]
942
- if checksum.get("changed_paths"):
943
- lines += [
944
- "#### Differences",
945
- "```text",
946
- "\n".join(checksum.get("changed_paths") or []),
947
- "```",
948
- "",
949
- ]
950
-
951
- lines += [
952
- "## Step Logs",
953
- "",
954
- ]
955
-
956
- for i, step_row in enumerate(entry.steps, 1):
957
- sid = step_row.get("id", "")
958
- status = (step_row.get("status") or "").upper()
959
- action = step_row.get("action", "")
960
- model_step = by_id.get(sid)
961
- desc = model_step.description if model_step else ""
962
- cmd = _step_command_block(model_step) if model_step else f"action: {action}"
963
- result = (step_row.get("result") or "").strip()
964
- error = (step_row.get("error") or "").strip()
965
- log_text = error or result or "(no output captured)"
966
-
967
- lines += [
968
- f"### {i}. {sid} [{status}]",
969
- "",
970
- f"- Action: {action}",
971
- f"- Description: {desc}",
972
- "",
973
- "#### Executed",
974
- "```bash",
975
- cmd,
976
- "```",
977
- "",
978
- "#### Output",
979
- "```text",
980
- log_text,
981
- "```",
982
- "",
983
- ]
984
-
985
- return "\n".join(lines)
986
-
987
-
988
- def _write_markdown_report(
989
- console,
990
- migration,
991
- spec_path: Path,
992
- started_at: datetime,
993
- ok: bool,
994
- executed: bool,
995
- report_file: Path | None = None,
996
- ) -> None:
997
- entry = _resolve_audit_entry(migration, started_at=started_at, ok=ok)
998
- checksum = _build_checksum_verification(migration, executed=executed)
999
- out_path = report_file or _default_report_path(spec_path)
1000
- out_path.parent.mkdir(parents=True, exist_ok=True)
1001
- out_path.write_text(
1002
- _render_markdown_report(entry, migration, spec_path, checksum=checksum),
1003
- encoding="utf-8",
1004
- )
1005
- console.print(f"[bold]report[/bold] [dim]saved → {out_path}[/dim]")