redeploy 0.2.57__tar.gz → 0.2.58__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.57 → redeploy-0.2.58}/PKG-INFO +5 -5
- {redeploy-0.2.57 → redeploy-0.2.58}/README.md +4 -4
- {redeploy-0.2.57 → redeploy-0.2.58}/pyproject.toml +1 -1
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/__init__.py +1 -1
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/iac/docker_compose.py +37 -4
- redeploy-0.2.58/redeploy/models/blueprint.py +95 -0
- redeploy-0.2.58/redeploy/models/hardware.py +117 -0
- redeploy-0.2.58/redeploy/models/infra.py +77 -0
- redeploy-0.2.58/redeploy/models/manifest.py +151 -0
- redeploy-0.2.58/redeploy/models/pipeline.py +35 -0
- redeploy-0.2.58/redeploy/models/plan.py +83 -0
- redeploy-0.2.58/redeploy/models/spec.py +182 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy.egg-info/PKG-INFO +5 -5
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy.egg-info/SOURCES.txt +7 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/handlers.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/audit.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/sources/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/sources/compose.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/sources/hardware.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/sources/infra.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/blueprint/sources/migration.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/bump_fix.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/device_map_renderers.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/hardware.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/mcp_cmd.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/prompt_cmd.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli/query.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/cli.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/discovery.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/fleet.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/heal/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/heal/decider.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/heal/hint_provider.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/heal/log_writer.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/heal/loop_detector.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/heal/runner.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/heal.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/iac/config_hints.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/integrations/op3_bridge.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/mcp_server.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/models/enums.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/models/persisted.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/models.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/observe.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/parse.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/patterns.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/schema.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/ssh.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/steps/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/steps/builtins.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/steps/kiosk.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_heal.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_hooks.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_iac_compose_contract.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/verify.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy/version.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/setup.cfg +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_blueprint_sources.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_blueprint_sources_compose.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_cli_query.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_example_argocd_flux_plugin.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_example_gitops_ci_plugin.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_example_helm_kustomize_plugin.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_heal_loop_detector.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_iac.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_iac_config_hints.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_iac_plugin_loading.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_import_plugin_template.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_parse.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_prompt_llm.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/tests/test_redeploy.py +0 -0
- {redeploy-0.2.57 → redeploy-0.2.58}/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.58
|
|
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
36
|
  
|
|
37
37
|
|
|
38
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
39
|
-
- 👤 **Human dev:** ~$
|
|
38
|
+
- 🤖 **LLM usage:** $7.5000 (78 commits)
|
|
39
|
+
- 👤 **Human dev:** ~$2890 (28.9h @ $100/h, 30min dedup)
|
|
40
40
|
|
|
41
41
|
Generated on 2026-04-21 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
7
|
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.5000 (78 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2890 (28.9h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-21 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.58"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -399,6 +399,40 @@ class DockerComposeParser:
|
|
|
399
399
|
def _parse_service_command(self, cfg: dict) -> Optional[str]:
|
|
400
400
|
return str(cfg["command"]) if cfg.get("command") else None
|
|
401
401
|
|
|
402
|
+
def _build_service_info(
|
|
403
|
+
self,
|
|
404
|
+
*,
|
|
405
|
+
name: str,
|
|
406
|
+
cfg: dict,
|
|
407
|
+
image: Optional[str],
|
|
408
|
+
ports: list[PortInfo],
|
|
409
|
+
volumes: list[VolumeInfo],
|
|
410
|
+
env: dict[str, str],
|
|
411
|
+
env_files: list[str],
|
|
412
|
+
svc_networks: list[str],
|
|
413
|
+
depends_on: list[str],
|
|
414
|
+
healthcheck: Optional[str],
|
|
415
|
+
build_context: Optional[str],
|
|
416
|
+
replicas: int,
|
|
417
|
+
labels: dict[str, str],
|
|
418
|
+
) -> ServiceInfo:
|
|
419
|
+
return ServiceInfo(
|
|
420
|
+
name=name,
|
|
421
|
+
image=image,
|
|
422
|
+
ports=ports,
|
|
423
|
+
volumes=volumes,
|
|
424
|
+
env=env,
|
|
425
|
+
env_files=env_files,
|
|
426
|
+
networks=svc_networks,
|
|
427
|
+
depends_on=depends_on,
|
|
428
|
+
healthcheck=healthcheck,
|
|
429
|
+
restart=cfg.get("restart"),
|
|
430
|
+
command=self._parse_service_command(cfg),
|
|
431
|
+
build_context=build_context,
|
|
432
|
+
replicas=replicas,
|
|
433
|
+
labels=labels,
|
|
434
|
+
)
|
|
435
|
+
|
|
402
436
|
def _parse_service(self, name: str, cfg: dict,
|
|
403
437
|
env_context: dict[str, str],
|
|
404
438
|
spec: ParsedSpec) -> ServiceInfo:
|
|
@@ -413,18 +447,17 @@ class DockerComposeParser:
|
|
|
413
447
|
replicas = self._parse_service_replicas(cfg)
|
|
414
448
|
labels = self._parse_service_labels(cfg)
|
|
415
449
|
|
|
416
|
-
return
|
|
450
|
+
return self._build_service_info(
|
|
417
451
|
name=name,
|
|
452
|
+
cfg=cfg,
|
|
418
453
|
image=image,
|
|
419
454
|
ports=ports,
|
|
420
455
|
volumes=volumes,
|
|
421
456
|
env=env,
|
|
422
457
|
env_files=env_files,
|
|
423
|
-
|
|
458
|
+
svc_networks=svc_networks,
|
|
424
459
|
depends_on=depends_on,
|
|
425
460
|
healthcheck=healthcheck,
|
|
426
|
-
restart=cfg.get("restart"),
|
|
427
|
-
command=self._parse_service_command(cfg),
|
|
428
461
|
build_context=build_context,
|
|
429
462
|
replicas=replicas,
|
|
430
463
|
labels=labels,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Device blueprint models — portable deployment recipes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from .hardware import HardwareInfo
|
|
11
|
+
from .persisted import PersistedModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_DEFAULT_BLUEPRINT_DIR = Path.home() / ".config" / "redeploy" / "blueprints"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ServicePort(BaseModel):
|
|
18
|
+
host: int
|
|
19
|
+
container: int
|
|
20
|
+
protocol: str = "tcp"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VolumeMount(BaseModel):
|
|
24
|
+
host: str
|
|
25
|
+
container: str
|
|
26
|
+
read_only: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ServiceSpec(BaseModel):
|
|
30
|
+
name: str
|
|
31
|
+
image: str
|
|
32
|
+
platform: str = ""
|
|
33
|
+
ports: list[ServicePort] = Field(default_factory=list)
|
|
34
|
+
volumes: list[VolumeMount] = Field(default_factory=list)
|
|
35
|
+
env: dict[str, str] = Field(default_factory=dict)
|
|
36
|
+
command: Optional[str] = None
|
|
37
|
+
healthcheck: Optional[str] = None
|
|
38
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
39
|
+
labels: dict[str, str] = Field(default_factory=dict)
|
|
40
|
+
restart: str = "unless-stopped"
|
|
41
|
+
network_mode: Optional[str] = None
|
|
42
|
+
privileged: bool = False
|
|
43
|
+
source_ref: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HardwareRequirements(BaseModel):
|
|
47
|
+
arch: str = "linux/arm64"
|
|
48
|
+
min_ram_mb: int = 0
|
|
49
|
+
min_disk_gb: int = 0
|
|
50
|
+
display_type: Optional[str] = None
|
|
51
|
+
display_resolution: Optional[str] = None
|
|
52
|
+
i2c_required: bool = False
|
|
53
|
+
gpio_required: bool = False
|
|
54
|
+
features: list[str] = Field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BlueprintSource(BaseModel):
|
|
58
|
+
device_id: Optional[str] = None
|
|
59
|
+
device_name: Optional[str] = None
|
|
60
|
+
migration_file: Optional[str] = None
|
|
61
|
+
markpact_files: list[str] = Field(default_factory=list)
|
|
62
|
+
compose_files: list[str] = Field(default_factory=list)
|
|
63
|
+
extracted_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DeviceBlueprint(PersistedModel):
|
|
67
|
+
"""Self-contained, portable deployment recipe."""
|
|
68
|
+
name: str
|
|
69
|
+
version: str = "0.0.0"
|
|
70
|
+
description: str = ""
|
|
71
|
+
tags: list[str] = Field(default_factory=list)
|
|
72
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
73
|
+
|
|
74
|
+
services: list[ServiceSpec] = Field(default_factory=list)
|
|
75
|
+
hardware: HardwareRequirements = Field(default_factory=HardwareRequirements)
|
|
76
|
+
app_url: Optional[str] = None
|
|
77
|
+
deploy_strategy: str = "podman_quadlet"
|
|
78
|
+
env_file: Optional[str] = None
|
|
79
|
+
notes: str = ""
|
|
80
|
+
source: BlueprintSource = Field(default_factory=BlueprintSource)
|
|
81
|
+
|
|
82
|
+
def service(self, name: str) -> Optional[ServiceSpec]:
|
|
83
|
+
return next((s for s in self.services if s.name == name), None)
|
|
84
|
+
|
|
85
|
+
def save(self, path: Optional[Path] = None) -> Path:
|
|
86
|
+
p = path or _DEFAULT_BLUEPRINT_DIR / f"{self.name}.blueprint.yaml"
|
|
87
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
p.write_text(self.to_yaml())
|
|
89
|
+
return p
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def list_saved(cls) -> list[Path]:
|
|
93
|
+
if not _DEFAULT_BLUEPRINT_DIR.exists():
|
|
94
|
+
return []
|
|
95
|
+
return sorted(_DEFAULT_BLUEPRINT_DIR.glob("*.blueprint.yaml"))
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Hardware models — output of hardware probe."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DrmOutput(BaseModel):
|
|
11
|
+
"""One DRM connector (e.g. card1-DSI-2, card2-HDMI-A-1)."""
|
|
12
|
+
name: str # e.g. "card1-DSI-2"
|
|
13
|
+
connector: str # e.g. "DSI-2", "HDMI-A-1"
|
|
14
|
+
status: str = "unknown" # connected / disconnected
|
|
15
|
+
enabled: str = "unknown" # enabled / disabled
|
|
16
|
+
modes: list[str] = Field(default_factory=list) # e.g. ["1280x800@60"]
|
|
17
|
+
transform: str = "normal"
|
|
18
|
+
position: str = "0,0"
|
|
19
|
+
scale: str = "1.0"
|
|
20
|
+
edid_bytes: int = 0 # EDID size in bytes; 0 = no physical panel
|
|
21
|
+
power_state: Optional[str] = None # DPMS: "on", "off", "suspend"
|
|
22
|
+
sysfs_path: str = "" # e.g. /sys/class/drm/card1-DSI-2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BacklightInfo(BaseModel):
|
|
26
|
+
"""Sysfs backlight device."""
|
|
27
|
+
name: str # e.g. "11-0045"
|
|
28
|
+
brightness: int = 0
|
|
29
|
+
max_brightness: int = 255
|
|
30
|
+
bl_power: int = 0 # 0=on, 4=off
|
|
31
|
+
display_name: Optional[str] = None # e.g. "DSI-2"
|
|
32
|
+
sysfs_path: str = "" # e.g. /sys/class/backlight/11-0045
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class I2CBusInfo(BaseModel):
|
|
36
|
+
bus: int
|
|
37
|
+
devices: list[str] = Field(default_factory=list) # hex addresses found
|
|
38
|
+
sysfs_path: str = "" # e.g. /dev/i2c-11
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class HardwareDiagnostic(BaseModel):
|
|
42
|
+
"""Problem found during hardware probe."""
|
|
43
|
+
component: str # "dsi", "backlight", "i2c", "gpio", "overlay"
|
|
44
|
+
severity: str = "warning" # info / warning / error / critical
|
|
45
|
+
message: str
|
|
46
|
+
fix: Optional[str] = None # suggested fix command or action
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HardwareInfo(BaseModel):
|
|
50
|
+
"""Hardware state produced by hardware probe."""
|
|
51
|
+
board: Optional[str] = None # e.g. "Raspberry Pi 5 Model B Rev 1.0"
|
|
52
|
+
kernel: Optional[str] = None # uname -r
|
|
53
|
+
config_txt: str = "" # full /boot/firmware/config.txt
|
|
54
|
+
config_txt_path: str = "/boot/firmware/config.txt"
|
|
55
|
+
|
|
56
|
+
# Display
|
|
57
|
+
drm_outputs: list[DrmOutput] = Field(default_factory=list)
|
|
58
|
+
backlights: list[BacklightInfo] = Field(default_factory=list)
|
|
59
|
+
framebuffers: list[str] = Field(default_factory=list) # /dev/fb* names
|
|
60
|
+
wlr_outputs: list[dict] = Field(default_factory=list) # raw wlr-randr output
|
|
61
|
+
|
|
62
|
+
# DSI specific
|
|
63
|
+
dsi_overlays: list[str] = Field(default_factory=list) # active dtoverlay lines
|
|
64
|
+
dsi_dmesg: list[str] = Field(default_factory=list) # relevant dmesg lines
|
|
65
|
+
dsi_dmesg_errors: list[str] = Field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
# I2C
|
|
68
|
+
i2c_buses: list[I2CBusInfo] = Field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
# Kernel modules
|
|
71
|
+
kernel_modules: list[str] = Field(default_factory=list) # lsmod filtered
|
|
72
|
+
|
|
73
|
+
# Wayland/compositor runtime state
|
|
74
|
+
wayland_sockets: list[str] = Field(default_factory=list) # e.g. ["wayland-0"]
|
|
75
|
+
compositor_processes: dict[str, list[int]] = Field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
# Diagnostics
|
|
78
|
+
diagnostics: list[HardwareDiagnostic] = Field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def has_dsi(self) -> bool:
|
|
82
|
+
return any("DSI" in o.name for o in self.drm_outputs)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def kms_enabled(self) -> bool:
|
|
86
|
+
"""True if vc4-kms-v3d overlay is present in config.txt."""
|
|
87
|
+
return any(
|
|
88
|
+
re.match(r'\s*dtoverlay=vc4-kms-v3d', line)
|
|
89
|
+
for line in self.config_txt.splitlines()
|
|
90
|
+
if not line.strip().startswith("#")
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def dsi_connected(self) -> bool:
|
|
95
|
+
"""DRM reports connected — always True on RPi5 when DSI overlay is loaded."""
|
|
96
|
+
return any("DSI" in o.name and o.status == "connected" for o in self.drm_outputs)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def dsi_physically_connected(self) -> bool:
|
|
100
|
+
"""True only when a physical panel is present (EDID > 0 bytes)."""
|
|
101
|
+
return any("DSI" in o.name and o.edid_bytes > 0 for o in self.drm_outputs)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def dsi_enabled(self) -> bool:
|
|
105
|
+
return any("DSI" in o.name and o.enabled == "enabled" for o in self.drm_outputs)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def backlight_on(self) -> bool:
|
|
109
|
+
return any(b.bl_power == 0 and b.brightness > 0 for b in self.backlights)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def errors(self) -> list[HardwareDiagnostic]:
|
|
113
|
+
return [d for d in self.diagnostics if d.severity in ("error", "critical")]
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def warnings(self) -> list[HardwareDiagnostic]:
|
|
117
|
+
return [d for d in self.diagnostics if d.severity == "warning"]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Infrastructure state models — output of `detect`."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from .enums import ConflictSeverity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ServiceInfo(BaseModel):
|
|
13
|
+
name: str
|
|
14
|
+
image: Optional[str] = None
|
|
15
|
+
status: str = "unknown"
|
|
16
|
+
version: Optional[str] = None
|
|
17
|
+
ports: list[int] = Field(default_factory=list)
|
|
18
|
+
labels: dict[str, str] = Field(default_factory=dict)
|
|
19
|
+
namespace: Optional[str] = None # k3s/k8s namespace
|
|
20
|
+
unit: Optional[str] = None # systemd unit name
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PortInfo(BaseModel):
|
|
24
|
+
port: int
|
|
25
|
+
process: str
|
|
26
|
+
pid: Optional[int] = None
|
|
27
|
+
via: Optional[str] = None # "traefik", "nginx", "ingress-nginx"
|
|
28
|
+
protocol: str = "tcp"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConflictInfo(BaseModel):
|
|
32
|
+
type: str # "port_steal", "version_mismatch", "duplicate_service"
|
|
33
|
+
description: str
|
|
34
|
+
severity: ConflictSeverity = ConflictSeverity.MEDIUM
|
|
35
|
+
affected: list[str] = Field(default_factory=list)
|
|
36
|
+
fix_hint: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RuntimeInfo(BaseModel):
|
|
40
|
+
docker: Optional[str] = None
|
|
41
|
+
docker_compose: Optional[str] = None
|
|
42
|
+
k3s: Optional[str] = None
|
|
43
|
+
k3s_namespaces: list[str] = Field(default_factory=list)
|
|
44
|
+
podman: Optional[str] = None
|
|
45
|
+
systemd: Optional[str] = None
|
|
46
|
+
os: Optional[str] = None
|
|
47
|
+
arch: Optional[str] = None
|
|
48
|
+
chromium: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AppHealthInfo(BaseModel):
|
|
52
|
+
url: str
|
|
53
|
+
status_code: Optional[int] = None
|
|
54
|
+
version: Optional[str] = None
|
|
55
|
+
healthy: bool = False
|
|
56
|
+
response_ms: Optional[float] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class InfraState(BaseModel):
|
|
60
|
+
"""Full detected state of infrastructure — output of `detect`."""
|
|
61
|
+
host: str # "root@1.2.3.4" or "local"
|
|
62
|
+
app: str = "unknown"
|
|
63
|
+
scanned_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
64
|
+
|
|
65
|
+
runtime: RuntimeInfo = Field(default_factory=RuntimeInfo)
|
|
66
|
+
ports: dict[int, PortInfo] = Field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
services: dict[str, list[ServiceInfo]] = Field(
|
|
69
|
+
default_factory=lambda: {"docker": [], "k3s": [], "systemd": [], "podman": []}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
health: list[AppHealthInfo] = Field(default_factory=list)
|
|
73
|
+
conflicts: list[ConflictInfo] = Field(default_factory=list)
|
|
74
|
+
raw: dict[str, Any] = Field(default_factory=dict) # raw probe output
|
|
75
|
+
|
|
76
|
+
detected_strategy: str = "unknown" # DeployStrategy value
|
|
77
|
+
current_version: Optional[str] = None
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Project manifest models — redeploy.yaml + environments."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from .enums import DeployStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnvironmentConfig(BaseModel):
|
|
14
|
+
"""One named environment in redeploy.yaml."""
|
|
15
|
+
host: Optional[str] = None
|
|
16
|
+
strategy: Optional[str] = None
|
|
17
|
+
app: Optional[str] = None
|
|
18
|
+
domain: Optional[str] = None
|
|
19
|
+
remote_dir: Optional[str] = None
|
|
20
|
+
env_file: Optional[str] = None
|
|
21
|
+
ssh_key: Optional[str] = None
|
|
22
|
+
ssh_port: int = 22
|
|
23
|
+
verify_url: Optional[str] = None
|
|
24
|
+
spec: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProjectManifest(BaseModel):
|
|
28
|
+
"""Per-project redeploy.yaml — replaces repetitive Makefile variables."""
|
|
29
|
+
spec: str = "migration.yaml"
|
|
30
|
+
local_spec: str = "migration-local.yaml"
|
|
31
|
+
host: Optional[str] = None
|
|
32
|
+
app: str = "app"
|
|
33
|
+
domain: Optional[str] = None
|
|
34
|
+
ssh_key: Optional[str] = None
|
|
35
|
+
ssh_port: int = 22
|
|
36
|
+
remote_dir: Optional[str] = None
|
|
37
|
+
env_file: str = ".env"
|
|
38
|
+
environments: dict[str, EnvironmentConfig] = Field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def find_and_load(cls, start: Path) -> Optional[ProjectManifest]:
|
|
42
|
+
"""Walk up from *start* looking for redeploy.css/less/yaml."""
|
|
43
|
+
for d in [Path(start)] + list(Path(start).parents):
|
|
44
|
+
for css_name in ("redeploy.css", "redeploy.less"):
|
|
45
|
+
candidate = d / css_name
|
|
46
|
+
if candidate.exists():
|
|
47
|
+
from ..dsl.loader import load_css
|
|
48
|
+
result = load_css(candidate)
|
|
49
|
+
if result.manifest:
|
|
50
|
+
return result.manifest
|
|
51
|
+
candidate = d / "redeploy.yaml"
|
|
52
|
+
if candidate.exists():
|
|
53
|
+
with candidate.open() as f:
|
|
54
|
+
return cls(**yaml.safe_load(f))
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def find_css(cls, start: Path) -> Optional[Path]:
|
|
59
|
+
"""Return path to redeploy.css/less if found, else None."""
|
|
60
|
+
for d in [Path(start)] + list(Path(start).parents):
|
|
61
|
+
for name in ("redeploy.css", "redeploy.less"):
|
|
62
|
+
p = d / name
|
|
63
|
+
if p.exists():
|
|
64
|
+
return p
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def env(self, name: str) -> Optional[EnvironmentConfig]:
|
|
68
|
+
return self.environments.get(name)
|
|
69
|
+
|
|
70
|
+
def resolve_env(self, name: str) -> EnvironmentConfig:
|
|
71
|
+
base = EnvironmentConfig(
|
|
72
|
+
host=self.host,
|
|
73
|
+
app=self.app,
|
|
74
|
+
domain=self.domain,
|
|
75
|
+
remote_dir=self.remote_dir,
|
|
76
|
+
env_file=self.env_file,
|
|
77
|
+
ssh_key=self.ssh_key,
|
|
78
|
+
ssh_port=self.ssh_port,
|
|
79
|
+
)
|
|
80
|
+
override = self.environments.get(name)
|
|
81
|
+
if not override:
|
|
82
|
+
return base
|
|
83
|
+
return EnvironmentConfig(
|
|
84
|
+
host=override.host or base.host,
|
|
85
|
+
strategy=override.strategy,
|
|
86
|
+
app=override.app or base.app,
|
|
87
|
+
domain=override.domain or base.domain,
|
|
88
|
+
remote_dir=override.remote_dir or base.remote_dir,
|
|
89
|
+
env_file=override.env_file or base.env_file,
|
|
90
|
+
ssh_key=override.ssh_key or base.ssh_key,
|
|
91
|
+
ssh_port=override.ssh_port if override.ssh_port != 22 else base.ssh_port,
|
|
92
|
+
verify_url=override.verify_url,
|
|
93
|
+
spec=override.spec,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_dotenv(cls, project_dir: Path) -> Optional[ProjectManifest]:
|
|
98
|
+
"""Read DEPLOY_* vars from .env file in project_dir as a fallback."""
|
|
99
|
+
env_path = Path(project_dir) / ".env"
|
|
100
|
+
if not env_path.exists():
|
|
101
|
+
return None
|
|
102
|
+
data: dict = {}
|
|
103
|
+
for line in env_path.read_text().splitlines():
|
|
104
|
+
line = line.strip()
|
|
105
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
106
|
+
continue
|
|
107
|
+
k, _, v = line.partition("=")
|
|
108
|
+
k = k.strip().upper()
|
|
109
|
+
v = v.strip().strip('"').strip("'")
|
|
110
|
+
if k == "DEPLOY_HOST":
|
|
111
|
+
data["host"] = v
|
|
112
|
+
elif k == "DEPLOY_APP":
|
|
113
|
+
data["app"] = v
|
|
114
|
+
elif k == "DEPLOY_DOMAIN":
|
|
115
|
+
data["domain"] = v
|
|
116
|
+
elif k == "DEPLOY_ENV_FILE":
|
|
117
|
+
data["env_file"] = v
|
|
118
|
+
elif k == "DEPLOY_SSH_KEY":
|
|
119
|
+
data["ssh_key"] = v
|
|
120
|
+
if not data:
|
|
121
|
+
return None
|
|
122
|
+
return cls(**data)
|
|
123
|
+
|
|
124
|
+
def apply_to_spec(self, spec: Any, env_name: str = "") -> None:
|
|
125
|
+
"""Overlay manifest values onto a MigrationSpec."""
|
|
126
|
+
from .spec import MigrationSpec
|
|
127
|
+
assert isinstance(spec, MigrationSpec)
|
|
128
|
+
cfg = self.resolve_env(env_name) if env_name else self
|
|
129
|
+
host = cfg.host if isinstance(cfg, EnvironmentConfig) else self.host
|
|
130
|
+
domain = cfg.domain if isinstance(cfg, EnvironmentConfig) else self.domain
|
|
131
|
+
remote_dir = cfg.remote_dir if isinstance(cfg, EnvironmentConfig) else self.remote_dir
|
|
132
|
+
env_file = cfg.env_file if isinstance(cfg, EnvironmentConfig) else self.env_file
|
|
133
|
+
verify_url = cfg.verify_url if isinstance(cfg, EnvironmentConfig) else None
|
|
134
|
+
strategy_str = cfg.strategy if isinstance(cfg, EnvironmentConfig) else None
|
|
135
|
+
|
|
136
|
+
if host:
|
|
137
|
+
spec.source.host = host
|
|
138
|
+
spec.target.host = host
|
|
139
|
+
if domain and not spec.target.domain:
|
|
140
|
+
spec.target.domain = domain
|
|
141
|
+
if remote_dir and not spec.target.remote_dir:
|
|
142
|
+
spec.target.remote_dir = remote_dir
|
|
143
|
+
if env_file and not spec.target.env_file:
|
|
144
|
+
spec.target.env_file = env_file
|
|
145
|
+
if verify_url and not spec.target.verify_url:
|
|
146
|
+
spec.target.verify_url = verify_url
|
|
147
|
+
if strategy_str:
|
|
148
|
+
try:
|
|
149
|
+
spec.target.strategy = DeployStrategy(strategy_str)
|
|
150
|
+
except ValueError:
|
|
151
|
+
pass
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Pipeline hook models — generic pre/post/failure lifecycle."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Literal, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PipelinePhase = Literal[
|
|
10
|
+
"before_plan",
|
|
11
|
+
"before_apply",
|
|
12
|
+
"before_step",
|
|
13
|
+
"after_step",
|
|
14
|
+
"on_step_failure",
|
|
15
|
+
"on_step_retry",
|
|
16
|
+
"after_apply",
|
|
17
|
+
"on_failure",
|
|
18
|
+
"always",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Hook(BaseModel):
|
|
23
|
+
"""Generyczny hook w pipeline: faza + akcja + opcjonalny warunek.
|
|
24
|
+
|
|
25
|
+
Zastępuje ad-hoc pola typu ``post_deploy``/``pre_deploy``/``insert_before``.
|
|
26
|
+
Dodatkowe pola specyficzne dla akcji (command, url, ...) idą do ``extra``.
|
|
27
|
+
"""
|
|
28
|
+
id: str
|
|
29
|
+
phase: PipelinePhase
|
|
30
|
+
action: str # StepAction value (ssh_cmd, http_check, local_cmd, ...)
|
|
31
|
+
description: str = ""
|
|
32
|
+
when: Optional[str] = None # opcjonalny warunek
|
|
33
|
+
on_failure: Literal["abort", "continue", "warn"] = "warn"
|
|
34
|
+
|
|
35
|
+
model_config = {"extra": "allow"} # zachowuje dodatkowe pola (command, url, ...)
|