redeploy 0.2.66__tar.gz → 0.2.68__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.
- {redeploy-0.2.66 → redeploy-0.2.68}/PKG-INFO +6 -6
- {redeploy-0.2.66 → redeploy-0.2.68}/README.md +5 -5
- {redeploy-0.2.66 → redeploy-0.2.68}/pyproject.toml +1 -1
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/__init__.py +1 -1
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/analyze/spec_analyzer.py +21 -7
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/handlers.py +20 -6
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/plan_apply.py +367 -3
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/prompt_cmd.py +81 -5
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/schema.py +3 -1
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_analyze.py +53 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_executor.py +1 -1
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_prompt_llm.py +50 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/analyze/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/analyze/checkers/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/analyze/preflight_schema.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/audit.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/sources/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/sources/compose.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/sources/hardware.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/sources/infra.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/blueprint/sources/migration.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/bump_fix.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/device_map_renderers.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/hardware.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/lint.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/mcp_cmd.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/version/utils/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/version/utils/git_config.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli/query.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/cli.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/discovery.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/fleet.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/heal/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/heal/decider.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/heal/hint_provider.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/heal/log_writer.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/heal/loop_detector.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/heal/runner.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/heal.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/iac/config_hints.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/integrations/op3_bridge.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/mcp_server.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/blueprint.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/devices.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/enums.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/hardware.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/infra.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/manifest.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/persisted.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/pipeline.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/plan.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/models/spec.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/observe.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/parse.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/patterns.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/ssh.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/steps/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/steps/builtins.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/steps/kiosk.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_heal.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_hooks.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_iac_compose_contract.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/verify.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy/version.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy.egg-info/SOURCES.txt +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/setup.cfg +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_blueprint_sources.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_blueprint_sources_compose.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_cli_query.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_example_argocd_flux_plugin.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_example_gitops_ci_plugin.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_example_helm_kustomize_plugin.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_heal_loop_detector.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_iac.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_iac_config_hints.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_iac_plugin_loading.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_import_plugin_template.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_parse.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/tests/test_redeploy.py +0 -0
- {redeploy-0.2.66 → redeploy-0.2.68}/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.
|
|
3
|
+
Version: 0.2.68
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -32,17 +32,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
|
32
32
|
|
|
33
33
|
## AI Cost Tracking
|
|
34
34
|
|
|
35
|
-
     
|
|
36
|
+
  
|
|
37
37
|
|
|
38
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
39
|
-
- 👤 **Human dev:** ~$
|
|
38
|
+
- 🤖 **LLM usage:** $7.5000 (89 commits)
|
|
39
|
+
- 👤 **Human dev:** ~$3512 (35.1h @ $100/h, 30min dedup)
|
|
40
40
|
|
|
41
41
|
Generated on 2026-04-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
    
|
|
46
46
|
|
|
47
47
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
48
48
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
     
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.5000 (89 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$3512 (35.1h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -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.
|
|
10
|
+
__version__ = "0.2.68"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -293,6 +293,7 @@ class _ComposeChecker(_Checker):
|
|
|
293
293
|
"""Validate docker-compose files declared in spec or found in project."""
|
|
294
294
|
|
|
295
295
|
def check(self, spec, document, base_dir, result):
|
|
296
|
+
ign = _IgnoreList(base_dir)
|
|
296
297
|
compose_files = list(spec.target.compose_files or [])
|
|
297
298
|
# Also try to discover common names if none explicitly declared
|
|
298
299
|
if not compose_files:
|
|
@@ -310,15 +311,22 @@ class _ComposeChecker(_Checker):
|
|
|
310
311
|
suggestion=f"Create {path} or remove from target.compose_files."
|
|
311
312
|
)
|
|
312
313
|
continue
|
|
313
|
-
self._scan_compose(path, base_dir, result)
|
|
314
|
+
self._scan_compose(path, base_dir, result, ign)
|
|
314
315
|
|
|
315
|
-
def _scan_compose(self, path: Path, base_dir: Path, result: AnalysisResult):
|
|
316
|
+
def _scan_compose(self, path: Path, base_dir: Path, result: AnalysisResult, ign: _IgnoreList):
|
|
316
317
|
try:
|
|
317
318
|
data = yaml.safe_load(path.read_text()) or {}
|
|
318
319
|
except Exception as exc:
|
|
319
320
|
result.add(IssueSeverity.ERROR, "compose", f"Cannot parse {path}: {exc}")
|
|
320
321
|
return
|
|
321
322
|
|
|
323
|
+
def _skip_ignored(check_path: Path) -> bool:
|
|
324
|
+
try:
|
|
325
|
+
rel = check_path.resolve().relative_to(base_dir.resolve())
|
|
326
|
+
return ign.is_ignored(rel)
|
|
327
|
+
except ValueError:
|
|
328
|
+
return False
|
|
329
|
+
|
|
322
330
|
services = data.get("services", {})
|
|
323
331
|
for svc_name, svc in services.items():
|
|
324
332
|
if not isinstance(svc, dict):
|
|
@@ -337,7 +345,7 @@ class _ComposeChecker(_Checker):
|
|
|
337
345
|
dockerfile = build.get("dockerfile")
|
|
338
346
|
if dockerfile:
|
|
339
347
|
df_path = ctx_path / dockerfile
|
|
340
|
-
if not df_path.exists():
|
|
348
|
+
if not df_path.exists() and not _skip_ignored(df_path):
|
|
341
349
|
result.add(
|
|
342
350
|
IssueSeverity.ERROR, "compose",
|
|
343
351
|
f"Service '{svc_name}' Dockerfile missing: {df_path}",
|
|
@@ -349,7 +357,7 @@ class _ComposeChecker(_Checker):
|
|
|
349
357
|
env_files = [env_files]
|
|
350
358
|
for ef in env_files:
|
|
351
359
|
ef_path = base_dir / ef if not str(ef).startswith("/") else Path(ef)
|
|
352
|
-
if not ef_path.exists():
|
|
360
|
+
if not ef_path.exists() and not _skip_ignored(ef_path):
|
|
353
361
|
result.add(
|
|
354
362
|
IssueSeverity.WARNING, "compose",
|
|
355
363
|
f"Service '{svc_name}' env_file missing: {ef}",
|
|
@@ -362,7 +370,7 @@ class _ComposeChecker(_Checker):
|
|
|
362
370
|
host_part = vol.split(":", 1)[0]
|
|
363
371
|
# Skip named volumes and relative paths that exist
|
|
364
372
|
if host_part.startswith("/"):
|
|
365
|
-
if not Path(host_part).exists():
|
|
373
|
+
if not Path(host_part).exists() and not _skip_ignored(Path(host_part)):
|
|
366
374
|
result.add(
|
|
367
375
|
IssueSeverity.WARNING, "compose",
|
|
368
376
|
f"Service '{svc_name}' volume host path missing: {host_part}",
|
|
@@ -370,7 +378,7 @@ class _ComposeChecker(_Checker):
|
|
|
370
378
|
)
|
|
371
379
|
elif host_part != "." and "/" in host_part:
|
|
372
380
|
hp = base_dir / host_part
|
|
373
|
-
if not hp.exists():
|
|
381
|
+
if not hp.exists() and not _skip_ignored(hp):
|
|
374
382
|
result.add(
|
|
375
383
|
IssueSeverity.WARNING, "compose",
|
|
376
384
|
f"Service '{svc_name}' relative volume path missing: {hp}",
|
|
@@ -687,9 +695,11 @@ class SpecAnalyzer:
|
|
|
687
695
|
]
|
|
688
696
|
|
|
689
697
|
def __init__(self, base_dir: Path | None = None,
|
|
690
|
-
checkers: list[_Checker] | None = None
|
|
698
|
+
checkers: list[_Checker] | None = None,
|
|
699
|
+
auto_create_redeployignore: bool = True):
|
|
691
700
|
self.base_dir = base_dir or Path.cwd()
|
|
692
701
|
self.checkers = checkers or list(self.DEFAULT_CHECKERS)
|
|
702
|
+
self.auto_create_redeployignore = auto_create_redeployignore
|
|
693
703
|
|
|
694
704
|
def analyze(self, spec: MigrationSpec,
|
|
695
705
|
document: MarkpactDocument | None = None) -> AnalysisResult:
|
|
@@ -704,6 +714,10 @@ class SpecAnalyzer:
|
|
|
704
714
|
from ..markpact.compiler import compile_markpact_document, MarkpactCompileError
|
|
705
715
|
from ..models.spec import MigrationSpec as MS
|
|
706
716
|
|
|
717
|
+
# Ensure .redeployignore exists with sensible defaults
|
|
718
|
+
if self.auto_create_redeployignore:
|
|
719
|
+
ensure_redeployignore(self.base_dir)
|
|
720
|
+
|
|
707
721
|
document = None
|
|
708
722
|
spec = None
|
|
709
723
|
result = AnalysisResult()
|
|
@@ -21,6 +21,20 @@ if TYPE_CHECKING:
|
|
|
21
21
|
from .progress import ProgressEmitter
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def _format_step_output(stdout: str, stderr: str, max_chars: int = 8000) -> str:
|
|
25
|
+
out = str(stdout or "").strip()
|
|
26
|
+
err = str(stderr or "").strip()
|
|
27
|
+
chunks: list[str] = []
|
|
28
|
+
if out:
|
|
29
|
+
chunks.append("stdout:\n" + out)
|
|
30
|
+
if err:
|
|
31
|
+
chunks.append("stderr:\n" + err)
|
|
32
|
+
if not chunks:
|
|
33
|
+
return "ok"
|
|
34
|
+
joined = "\n\n".join(chunks)
|
|
35
|
+
return joined[:max_chars]
|
|
36
|
+
|
|
37
|
+
|
|
24
38
|
def run_ssh(step: MigrationStep, probe: RemoteProbe) -> None:
|
|
25
39
|
"""Execute SSH command on remote host."""
|
|
26
40
|
cmd = step.command
|
|
@@ -28,9 +42,9 @@ def run_ssh(step: MigrationStep, probe: RemoteProbe) -> None:
|
|
|
28
42
|
raise StepError(step, "No command specified")
|
|
29
43
|
timeout = step.timeout or 300
|
|
30
44
|
r = probe.run(cmd, timeout=timeout)
|
|
31
|
-
step.result = r.out
|
|
45
|
+
step.result = _format_step_output(r.out, r.stderr)
|
|
32
46
|
if not r.ok:
|
|
33
|
-
raise StepError(step, f"exit={r.exit_code}: {r.stderr
|
|
47
|
+
raise StepError(step, f"exit={r.exit_code}: {_format_step_output(r.out, r.stderr, 400)}")
|
|
34
48
|
step.status = StepStatus.DONE
|
|
35
49
|
|
|
36
50
|
|
|
@@ -51,9 +65,9 @@ def run_scp(step: MigrationStep, probe: RemoteProbe, plan: MigrationPlan) -> Non
|
|
|
51
65
|
step.src, f"{plan.host}:{step.dst}"]
|
|
52
66
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
53
67
|
if result.returncode != 0:
|
|
54
|
-
raise StepError(step, f"scp failed: {result.stderr
|
|
68
|
+
raise StepError(step, f"scp failed: {_format_step_output(result.stdout, result.stderr, 400)}")
|
|
55
69
|
step.status = StepStatus.DONE
|
|
56
|
-
step.result =
|
|
70
|
+
step.result = _format_step_output(result.stdout, result.stderr)
|
|
57
71
|
|
|
58
72
|
|
|
59
73
|
def run_rsync(step: MigrationStep, probe: RemoteProbe, plan: MigrationPlan) -> None:
|
|
@@ -80,9 +94,9 @@ def run_rsync(step: MigrationStep, probe: RemoteProbe, plan: MigrationPlan) -> N
|
|
|
80
94
|
cmd += [step.src, dst]
|
|
81
95
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
|
82
96
|
if result.returncode != 0:
|
|
83
|
-
raise StepError(step, f"rsync failed: {result.stderr
|
|
97
|
+
raise StepError(step, f"rsync failed: {_format_step_output(result.stdout, result.stderr, 400)}")
|
|
84
98
|
step.status = StepStatus.DONE
|
|
85
|
-
step.result =
|
|
99
|
+
step.result = _format_step_output(result.stdout, result.stderr)
|
|
86
100
|
|
|
87
101
|
|
|
88
102
|
def _ensure_remote_parent_dir(probe: RemoteProbe, remote_dst: str) -> None:
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""plan, apply, migrate, run commands — Migration planning and execution."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import subprocess
|
|
4
5
|
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
|
|
7
9
|
import click
|
|
10
|
+
from loguru import logger
|
|
8
11
|
from rich.console import Console
|
|
9
12
|
from rich.table import Table
|
|
10
13
|
|
|
@@ -234,6 +237,18 @@ def migrate(ctx, host, app, domain, target, strategy, target_version,
|
|
|
234
237
|
@click.option(
|
|
235
238
|
"-o", "--output", default=None, type=click.Path(), help="Save apply results to file"
|
|
236
239
|
)
|
|
240
|
+
@click.option(
|
|
241
|
+
"--report/--no-report", default=False, show_default=True,
|
|
242
|
+
help="Write Markdown execution report next to the spec file."
|
|
243
|
+
)
|
|
244
|
+
@click.option(
|
|
245
|
+
"--report-file", default=None, type=click.Path(),
|
|
246
|
+
help="Override Markdown report output path."
|
|
247
|
+
)
|
|
248
|
+
@click.option(
|
|
249
|
+
"--sync-project/--no-sync-project", default=True, show_default=True,
|
|
250
|
+
help="Before apply, sync full local project tree to target.remote_dir (respects .gitignore/.redeployignore)."
|
|
251
|
+
)
|
|
237
252
|
@click.option(
|
|
238
253
|
"--env", "env_name", default="",
|
|
239
254
|
help="Named environment from redeploy.yaml (e.g. prod, dev, rpi5)"
|
|
@@ -289,6 +304,7 @@ def migrate(ctx, host, app, domain, target, strategy, target_version,
|
|
|
289
304
|
)
|
|
290
305
|
@click.pass_context
|
|
291
306
|
def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
|
|
307
|
+
report, report_file, sync_project,
|
|
292
308
|
env_name, progress_yaml, resume, from_step, state_file, no_state,
|
|
293
309
|
heal, fix_hint, max_heal_retries, lint,
|
|
294
310
|
preflight, preflight_schema_out, preflight_remote, strict_preflight):
|
|
@@ -327,6 +343,19 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
|
|
|
327
343
|
sys.exit(1)
|
|
328
344
|
|
|
329
345
|
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)
|
|
330
359
|
|
|
331
360
|
# Overlay manifest values
|
|
332
361
|
_apply_manifest_to_spec(console, manifest, spec, env_name)
|
|
@@ -349,6 +378,7 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
|
|
|
349
378
|
console.print(f" [dim]→ {i.suggestion}[/dim]")
|
|
350
379
|
if not lint_result.passed:
|
|
351
380
|
console.print("\n[red]✗ lint failed — fix errors above or use --no-lint to skip.[/red]")
|
|
381
|
+
logger.remove(file_handler_id)
|
|
352
382
|
sys.exit(1)
|
|
353
383
|
else:
|
|
354
384
|
console.print(f"[yellow]⚠ lint passed with {len(lint_result.warnings())} warning(s)[/yellow]")
|
|
@@ -365,6 +395,10 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
|
|
|
365
395
|
console.print(f"\n[bold]plan[/bold]")
|
|
366
396
|
migration = planner.run()
|
|
367
397
|
|
|
398
|
+
# Keep remote app tree synchronized on every deployment run.
|
|
399
|
+
if sync_project:
|
|
400
|
+
_inject_project_sync_step(migration, spec, Path.cwd(), console)
|
|
401
|
+
|
|
368
402
|
if plan_out:
|
|
369
403
|
planner.save(migration, Path(plan_out))
|
|
370
404
|
console.print(f" [dim]plan saved → {plan_out}[/dim]")
|
|
@@ -396,17 +430,28 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
|
|
|
396
430
|
console.print(f" [dim]... and {blockers - 10} more blockers in schema file[/dim]")
|
|
397
431
|
if strict_preflight:
|
|
398
432
|
console.print("\n[red]✗ strict preflight failed — aborting before apply[/red]")
|
|
433
|
+
logger.remove(file_handler_id)
|
|
399
434
|
sys.exit(1)
|
|
400
435
|
else:
|
|
401
436
|
console.print("[green] ✓ preflight passed (no blockers)[/green]")
|
|
402
437
|
|
|
403
438
|
if plan_only:
|
|
439
|
+
if report:
|
|
440
|
+
_write_markdown_report(
|
|
441
|
+
console=console,
|
|
442
|
+
migration=migration,
|
|
443
|
+
spec_path=Path(resolved_spec),
|
|
444
|
+
started_at=started_at,
|
|
445
|
+
ok=True,
|
|
446
|
+
executed=False,
|
|
447
|
+
report_file=Path(report_file) if report_file else None,
|
|
448
|
+
)
|
|
404
449
|
console.print("\n[dim]--plan-only: stopping before apply[/dim]")
|
|
450
|
+
logger.remove(file_handler_id)
|
|
405
451
|
return
|
|
406
452
|
|
|
407
453
|
# Apply
|
|
408
454
|
load_user_plugins()
|
|
409
|
-
|
|
410
455
|
# Self-healing mode
|
|
411
456
|
if heal and not dry_run:
|
|
412
457
|
_load_dotenv()
|
|
@@ -441,7 +486,19 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
|
|
|
441
486
|
spec_path=str(resolved_spec)
|
|
442
487
|
)
|
|
443
488
|
|
|
489
|
+
if report:
|
|
490
|
+
_write_markdown_report(
|
|
491
|
+
console=console,
|
|
492
|
+
migration=migration,
|
|
493
|
+
spec_path=Path(resolved_spec),
|
|
494
|
+
started_at=started_at,
|
|
495
|
+
ok=ok,
|
|
496
|
+
executed=True,
|
|
497
|
+
report_file=Path(report_file) if report_file else None,
|
|
498
|
+
)
|
|
499
|
+
|
|
444
500
|
if not ok:
|
|
501
|
+
logger.remove(file_handler_id)
|
|
445
502
|
sys.exit(1)
|
|
446
503
|
|
|
447
504
|
|
|
@@ -599,8 +656,8 @@ def _ensure_redeployignore(project_root: Path, console) -> None:
|
|
|
599
656
|
".git/",
|
|
600
657
|
".venv/",
|
|
601
658
|
"venv/",
|
|
602
|
-
".env",
|
|
603
|
-
".
|
|
659
|
+
"# NOTE: .env is intentionally not excluded by default",
|
|
660
|
+
"# so deployment workflows can choose to sync it.",
|
|
604
661
|
"",
|
|
605
662
|
"# Python / Node caches",
|
|
606
663
|
"__pycache__/",
|
|
@@ -639,3 +696,310 @@ def _ensure_redeployignore(project_root: Path, console) -> None:
|
|
|
639
696
|
|
|
640
697
|
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
641
698
|
console.print(f"[dim]generated {target.name} (first run)[/dim]")
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _inject_project_sync_step(migration, spec, project_root: Path, console) -> None:
|
|
702
|
+
"""Inject a full-project rsync step unless already present.
|
|
703
|
+
|
|
704
|
+
This keeps remote tree aligned with local sources and catches missing files.
|
|
705
|
+
"""
|
|
706
|
+
from ...models import MigrationStep, StepAction
|
|
707
|
+
|
|
708
|
+
if any(s.id == "sync_project_tree" for s in migration.steps):
|
|
709
|
+
return
|
|
710
|
+
|
|
711
|
+
host = (migration.host or "").strip().lower()
|
|
712
|
+
if host in {"", "local", "localhost", "127.0.0.1"}:
|
|
713
|
+
return
|
|
714
|
+
|
|
715
|
+
remote_dir = (getattr(spec.target, "remote_dir", "") or "").strip()
|
|
716
|
+
if not remote_dir:
|
|
717
|
+
return
|
|
718
|
+
|
|
719
|
+
sync_step = MigrationStep(
|
|
720
|
+
id="sync_project_tree",
|
|
721
|
+
action=StepAction.RSYNC,
|
|
722
|
+
description="Sync full project tree to remote_dir (respect .gitignore/.redeployignore)",
|
|
723
|
+
src=f"{project_root.as_posix().rstrip('/')}/",
|
|
724
|
+
dst=f"{remote_dir.rstrip('/')}/",
|
|
725
|
+
excludes=[
|
|
726
|
+
".git/",
|
|
727
|
+
".redeploy/state/",
|
|
728
|
+
".redeploy/logs/",
|
|
729
|
+
],
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
insert_at = 0
|
|
733
|
+
for idx, step in enumerate(migration.steps):
|
|
734
|
+
if step.id == "sync_env":
|
|
735
|
+
insert_at = idx + 1
|
|
736
|
+
break
|
|
737
|
+
migration.steps.insert(insert_at, sync_step)
|
|
738
|
+
console.print("[dim]plan: injected sync_project_tree (full rsync before apply)[/dim]")
|
|
739
|
+
|
|
740
|
+
|
|
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]")
|