redeploy 0.2.74__tar.gz → 0.2.76__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.74 → redeploy-0.2.76}/PKG-INFO +8 -8
- {redeploy-0.2.74 → redeploy-0.2.76}/README.md +6 -6
- {redeploy-0.2.74 → redeploy-0.2.76}/pyproject.toml +2 -3
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/__init__.py +1 -1
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/analyze/__init__.py +2 -1
- redeploy-0.2.76/redeploy/analyze/checkers/__init__.py +47 -0
- redeploy-0.2.76/redeploy/analyze/checkers/base.py +19 -0
- redeploy-0.2.76/redeploy/analyze/checkers/binary.py +74 -0
- redeploy-0.2.76/redeploy/analyze/checkers/command_ref.py +83 -0
- redeploy-0.2.76/redeploy/analyze/checkers/compose.py +143 -0
- redeploy-0.2.76/redeploy/analyze/checkers/docker_build.py +160 -0
- redeploy-0.2.76/redeploy/analyze/checkers/env.py +23 -0
- redeploy-0.2.76/redeploy/analyze/checkers/path.py +94 -0
- redeploy-0.2.76/redeploy/analyze/checkers/reference.py +39 -0
- redeploy-0.2.76/redeploy/analyze/ignore.py +86 -0
- redeploy-0.2.76/redeploy/analyze/models.py +48 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/analyze/preflight_schema.py +1 -1
- redeploy-0.2.76/redeploy/analyze/spec_analyzer.py +118 -0
- redeploy-0.2.76/redeploy/audit/__init__.py +51 -0
- redeploy-0.2.76/redeploy/audit/auditor.py +155 -0
- redeploy-0.2.76/redeploy/audit/extractor.py +182 -0
- redeploy-0.2.76/redeploy/audit/models.py +81 -0
- redeploy-0.2.76/redeploy/audit/paths.py +34 -0
- redeploy-0.2.76/redeploy/audit/patterns.py +28 -0
- redeploy-0.2.76/redeploy/audit/probe.py +78 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/PKG-INFO +8 -8
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/SOURCES.txt +17 -1
- redeploy-0.2.74/redeploy/analyze/checkers/__init__.py +0 -1
- redeploy-0.2.74/redeploy/analyze/spec_analyzer.py +0 -742
- redeploy-0.2.74/redeploy/audit.py +0 -609
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/handlers.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/compose.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/hardware.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/infra.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/blueprint/sources/migration.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/bump_fix.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/device_map_renderers.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/gh_workflow.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/hardware.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/lint.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/mcp_cmd.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/prompt_cmd.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/utils/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/version/utils/git_config.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli/query.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/cli.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/handlers/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/handlers/display.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/discovery.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/fleet.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/decider.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/hint_provider.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/log_writer.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/loop_detector.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal/runner.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/heal.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/config_hints.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/integrations/op3_bridge.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/mcp_server.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/blueprint.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/devices.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/enums.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/hardware.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/infra.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/manifest.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/persisted.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/pipeline.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/plan.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/models/spec.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/observe.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/parse.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/patterns.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/schema.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/ssh.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/docker.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/generic.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/hardware.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/k3s.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/kiosk.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/podman.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/process.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/scm.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/steps/transfer.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_analyze.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_heal.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_hooks.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_iac_compose_contract.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_security_hardening.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_steps_registry.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/verify.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy/version.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/setup.cfg +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_blueprint_sources.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_blueprint_sources_compose.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_cli_gh_workflow.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_cli_query.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_example_argocd_flux_plugin.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_example_gitops_ci_plugin.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_example_helm_kustomize_plugin.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_heal_loop_detector.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_iac.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_iac_config_hints.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_iac_plugin_loading.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_import_plugin_template.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_parse.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_prompt_llm.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_redeploy.py +0 -0
- {redeploy-0.2.74 → redeploy-0.2.76}/tests/test_steps_kiosk.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.76
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
|
-
License: Apache-2.0
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: pydantic>=2.0
|
|
@@ -33,17 +33,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
|
33
33
|
|
|
34
34
|
## AI Cost Tracking
|
|
35
35
|
|
|
36
|
-
    
|
|
37
|
+
  
|
|
38
38
|
|
|
39
|
-
- 🤖 **LLM usage:** $
|
|
40
|
-
- 👤 **Human dev:** ~$
|
|
39
|
+
- 🤖 **LLM usage:** $5.4083 (98 commits)
|
|
40
|
+
- 👤 **Human dev:** ~$4198 (42.0h @ $100/h, 30min dedup)
|
|
41
41
|
|
|
42
|
-
Generated on 2026-
|
|
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
|
-
    
|
|
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
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $5.4083 (98 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$4198 (42.0h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-
|
|
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
|
-
    
|
|
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.
|
|
7
|
+
version = "0.2.76"
|
|
8
8
|
description = "Infrastructure migration toolkit: detect → plan → apply"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -22,6 +22,7 @@ dependencies = [
|
|
|
22
22
|
"costs>=0.1.20",
|
|
23
23
|
"pfix>=0.1.60",
|
|
24
24
|
]
|
|
25
|
+
license = "Apache-2.0"
|
|
25
26
|
|
|
26
27
|
[project.scripts]
|
|
27
28
|
redeploy = "redeploy.cli:cli"
|
|
@@ -36,8 +37,6 @@ dev = ["pytest>=8.0", "ruff>=0.4",
|
|
|
36
37
|
op3 = ["op3>=0.1.8"]
|
|
37
38
|
mcp = ["mcp>=1.0"]
|
|
38
39
|
|
|
39
|
-
[project.license]
|
|
40
|
-
text = "Apache-2.0"
|
|
41
40
|
[tool.ruff]
|
|
42
41
|
line-length = 100
|
|
43
42
|
|
|
@@ -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.76"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Static analysis for migration specs — pre-flight checks before plan/apply."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from .
|
|
4
|
+
from .models import AnalysisResult, IssueSeverity
|
|
5
|
+
from .spec_analyzer import SpecAnalyzer
|
|
5
6
|
from .preflight_schema import PreflightResult, generate_preflight_schema, save_preflight_schema
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Built-in static checkers for redeploy specs."""
|
|
2
|
+
from .base import Checker
|
|
3
|
+
from .binary import BinaryChecker, extract_binaries
|
|
4
|
+
from .command_ref import CommandRefChecker
|
|
5
|
+
from .compose import ComposeChecker, scan_compose_file
|
|
6
|
+
from .docker_build import DockerBuildChecker, collect_sync_mappings, parse_docker_build
|
|
7
|
+
from .env import EnvFileChecker
|
|
8
|
+
from .path import CommandPathChecker, PathChecker, is_inside, resolve_local_path
|
|
9
|
+
from .reference import ReferenceChecker
|
|
10
|
+
|
|
11
|
+
# Backward-compatible private aliases used in tests.
|
|
12
|
+
_Checker = Checker
|
|
13
|
+
_PathChecker = PathChecker
|
|
14
|
+
_CommandPathChecker = CommandPathChecker
|
|
15
|
+
_ReferenceChecker = ReferenceChecker
|
|
16
|
+
_ComposeChecker = ComposeChecker
|
|
17
|
+
_DockerBuildChecker = DockerBuildChecker
|
|
18
|
+
_CommandRefChecker = CommandRefChecker
|
|
19
|
+
_EnvFileChecker = EnvFileChecker
|
|
20
|
+
_BinaryChecker = BinaryChecker
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Checker",
|
|
24
|
+
"PathChecker",
|
|
25
|
+
"CommandPathChecker",
|
|
26
|
+
"ReferenceChecker",
|
|
27
|
+
"ComposeChecker",
|
|
28
|
+
"DockerBuildChecker",
|
|
29
|
+
"CommandRefChecker",
|
|
30
|
+
"EnvFileChecker",
|
|
31
|
+
"BinaryChecker",
|
|
32
|
+
"scan_compose_file",
|
|
33
|
+
"collect_sync_mappings",
|
|
34
|
+
"parse_docker_build",
|
|
35
|
+
"extract_binaries",
|
|
36
|
+
"resolve_local_path",
|
|
37
|
+
"is_inside",
|
|
38
|
+
"_Checker",
|
|
39
|
+
"_PathChecker",
|
|
40
|
+
"_CommandPathChecker",
|
|
41
|
+
"_ReferenceChecker",
|
|
42
|
+
"_ComposeChecker",
|
|
43
|
+
"_DockerBuildChecker",
|
|
44
|
+
"_CommandRefChecker",
|
|
45
|
+
"_EnvFileChecker",
|
|
46
|
+
"_BinaryChecker",
|
|
47
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Base class for static spec checkers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..models import AnalysisResult
|
|
7
|
+
from ...markpact.models import MarkpactDocument
|
|
8
|
+
from ...models.spec import MigrationSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Checker:
|
|
12
|
+
def check(
|
|
13
|
+
self,
|
|
14
|
+
spec: MigrationSpec,
|
|
15
|
+
document: MarkpactDocument | None,
|
|
16
|
+
base_dir: Path,
|
|
17
|
+
result: AnalysisResult,
|
|
18
|
+
) -> None:
|
|
19
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Local binary availability checks for shell commands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
from ..models import AnalysisResult, IssueSeverity
|
|
9
|
+
from .base import Checker
|
|
10
|
+
|
|
11
|
+
_WRAPPERS = frozenset({"sudo", "env", "command", "nohup", "time", "exec"})
|
|
12
|
+
_KEYWORDS = frozenset({
|
|
13
|
+
"if", "then", "else", "fi", "for", "do", "done", "while", "case", "esac",
|
|
14
|
+
"function", "in", "until", "select", "echo", "true", "false", "test", "[", "[[",
|
|
15
|
+
})
|
|
16
|
+
_SEPARATORS = frozenset({"&&", "||", "|", ";", "(", ")", "{", "}"})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extract_binaries(cmd: str) -> list[str]:
|
|
20
|
+
try:
|
|
21
|
+
tokens = shlex.split(cmd, posix=True)
|
|
22
|
+
except ValueError:
|
|
23
|
+
tokens = cmd.split()
|
|
24
|
+
|
|
25
|
+
out: list[str] = []
|
|
26
|
+
expect_cmd = True
|
|
27
|
+
i = 0
|
|
28
|
+
while i < len(tokens):
|
|
29
|
+
tok = tokens[i]
|
|
30
|
+
if tok in _SEPARATORS:
|
|
31
|
+
expect_cmd = True
|
|
32
|
+
i += 1
|
|
33
|
+
continue
|
|
34
|
+
if expect_cmd:
|
|
35
|
+
if tok in _KEYWORDS or tok.startswith("$"):
|
|
36
|
+
if tok == "for":
|
|
37
|
+
i += 1
|
|
38
|
+
i += 1
|
|
39
|
+
expect_cmd = False
|
|
40
|
+
continue
|
|
41
|
+
if "=" in tok and not tok.startswith("--") and "/" not in tok:
|
|
42
|
+
i += 1
|
|
43
|
+
continue
|
|
44
|
+
if tok in _WRAPPERS:
|
|
45
|
+
i += 1
|
|
46
|
+
continue
|
|
47
|
+
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_.-]*$", tok) and len(tok) > 1:
|
|
48
|
+
out.append(tok)
|
|
49
|
+
expect_cmd = False
|
|
50
|
+
i += 1
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BinaryChecker(Checker):
|
|
55
|
+
"""Warn if commands reference binaries not available locally (best-effort)."""
|
|
56
|
+
|
|
57
|
+
def check(self, spec, document, base_dir, result):
|
|
58
|
+
for step in spec.extra_steps:
|
|
59
|
+
action = str(step.get("action") or "")
|
|
60
|
+
if action in {"ensure_kanshi_profile"}:
|
|
61
|
+
continue
|
|
62
|
+
cmd = step.get("command") or ""
|
|
63
|
+
for binary in extract_binaries(cmd):
|
|
64
|
+
if shutil.which(binary) is None:
|
|
65
|
+
result.add(
|
|
66
|
+
IssueSeverity.WARNING,
|
|
67
|
+
"binaries",
|
|
68
|
+
f"Step '{step.get('id')}' command uses binary not found locally: '{binary}'",
|
|
69
|
+
step.get("id"),
|
|
70
|
+
suggestion=(
|
|
71
|
+
f"Install '{binary}' locally for lint parity, or ignore if it "
|
|
72
|
+
"exists only on remote host."
|
|
73
|
+
),
|
|
74
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""command_ref dependency validation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..models import AnalysisResult, IssueSeverity
|
|
7
|
+
from .base import Checker
|
|
8
|
+
from .path import resolve_local_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandRefChecker(Checker):
|
|
12
|
+
"""Validate command_ref references for nested markdown/script dependencies."""
|
|
13
|
+
|
|
14
|
+
def check(self, spec, document, base_dir, result):
|
|
15
|
+
from ...markpact.parser import resolve_script_ref
|
|
16
|
+
|
|
17
|
+
in_doc_refs: set[str] = set()
|
|
18
|
+
if document:
|
|
19
|
+
for block in document.blocks:
|
|
20
|
+
if block.kind == "ref" and block.ref_id:
|
|
21
|
+
in_doc_refs.add(block.ref_id)
|
|
22
|
+
|
|
23
|
+
for step in spec.extra_steps:
|
|
24
|
+
sid = step.get("id")
|
|
25
|
+
cref = step.get("command_ref")
|
|
26
|
+
if not cref:
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if "#" in cref:
|
|
30
|
+
self._check_external_ref(cref, sid, base_dir, document, in_doc_refs, result)
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
if document and cref not in in_doc_refs:
|
|
34
|
+
result.add(
|
|
35
|
+
IssueSeverity.WARNING,
|
|
36
|
+
"command_ref",
|
|
37
|
+
f"Step '{sid}' references unknown command_ref '{cref}'",
|
|
38
|
+
sid,
|
|
39
|
+
suggestion="Define a matching markpact:ref block in this spec.",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def _check_external_ref(
|
|
43
|
+
self,
|
|
44
|
+
cref: str,
|
|
45
|
+
sid: str | None,
|
|
46
|
+
base_dir: Path,
|
|
47
|
+
document,
|
|
48
|
+
in_doc_refs: set[str],
|
|
49
|
+
result: AnalysisResult,
|
|
50
|
+
) -> None:
|
|
51
|
+
from ...markpact.parser import resolve_script_ref
|
|
52
|
+
|
|
53
|
+
file_part, ref_id = cref.split("#", 1)
|
|
54
|
+
if file_part:
|
|
55
|
+
file_path = resolve_local_path(file_part, base_dir)
|
|
56
|
+
if not file_path.exists():
|
|
57
|
+
result.add(
|
|
58
|
+
IssueSeverity.ERROR,
|
|
59
|
+
"command_ref",
|
|
60
|
+
f"Step '{sid}' command_ref file missing: {file_part}",
|
|
61
|
+
sid,
|
|
62
|
+
suggestion=f"Create or fix path: {file_path}",
|
|
63
|
+
)
|
|
64
|
+
return
|
|
65
|
+
text = file_path.read_text(encoding="utf-8", errors="replace")
|
|
66
|
+
if resolve_script_ref(text, ref_id, language="bash") is None:
|
|
67
|
+
result.add(
|
|
68
|
+
IssueSeverity.ERROR,
|
|
69
|
+
"command_ref",
|
|
70
|
+
f"Step '{sid}' command_ref '#{ref_id}' not found in {file_part}",
|
|
71
|
+
sid,
|
|
72
|
+
suggestion="Add a matching markpact:ref bash block or section heading script.",
|
|
73
|
+
)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if document and ref_id not in in_doc_refs:
|
|
77
|
+
result.add(
|
|
78
|
+
IssueSeverity.WARNING,
|
|
79
|
+
"command_ref",
|
|
80
|
+
f"Step '{sid}' command_ref '#{ref_id}' not found as markpact:ref in current spec",
|
|
81
|
+
sid,
|
|
82
|
+
suggestion="If this is a heading-based script, keep as-is; otherwise add markpact:ref block.",
|
|
83
|
+
)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Docker Compose static validation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from ..ignore import IgnoreList
|
|
9
|
+
from ..models import AnalysisResult, IssueSeverity
|
|
10
|
+
from .base import Checker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_ignored(check_path: Path, base_dir: Path, ign: IgnoreList) -> bool:
|
|
14
|
+
try:
|
|
15
|
+
rel = check_path.resolve().relative_to(base_dir.resolve())
|
|
16
|
+
return ign.is_ignored(rel)
|
|
17
|
+
except ValueError:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _check_build_section(
|
|
22
|
+
svc_name: str,
|
|
23
|
+
svc: dict,
|
|
24
|
+
base_dir: Path,
|
|
25
|
+
result: AnalysisResult,
|
|
26
|
+
ign: IgnoreList,
|
|
27
|
+
) -> None:
|
|
28
|
+
build = svc.get("build")
|
|
29
|
+
if not isinstance(build, dict):
|
|
30
|
+
return
|
|
31
|
+
ctx = build.get("context", ".")
|
|
32
|
+
ctx_path = base_dir / ctx if not str(ctx).startswith("/") else Path(ctx)
|
|
33
|
+
if not ctx_path.exists():
|
|
34
|
+
result.add(
|
|
35
|
+
IssueSeverity.ERROR, "compose",
|
|
36
|
+
f"Service '{svc_name}' build.context missing: {ctx}",
|
|
37
|
+
suggestion=f"Create directory {ctx_path} or fix context.",
|
|
38
|
+
)
|
|
39
|
+
dockerfile = build.get("dockerfile")
|
|
40
|
+
if not dockerfile:
|
|
41
|
+
return
|
|
42
|
+
df_path = ctx_path / dockerfile
|
|
43
|
+
if not df_path.exists() and not _is_ignored(df_path, base_dir, ign):
|
|
44
|
+
result.add(
|
|
45
|
+
IssueSeverity.ERROR, "compose",
|
|
46
|
+
f"Service '{svc_name}' Dockerfile missing: {df_path}",
|
|
47
|
+
suggestion=f"Create {df_path} or correct dockerfile path.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _check_env_files(
|
|
52
|
+
svc_name: str,
|
|
53
|
+
svc: dict,
|
|
54
|
+
base_dir: Path,
|
|
55
|
+
result: AnalysisResult,
|
|
56
|
+
ign: IgnoreList,
|
|
57
|
+
) -> None:
|
|
58
|
+
env_files = svc.get("env_file", [])
|
|
59
|
+
if isinstance(env_files, str):
|
|
60
|
+
env_files = [env_files]
|
|
61
|
+
for ef in env_files:
|
|
62
|
+
ef_path = base_dir / ef if not str(ef).startswith("/") else Path(ef)
|
|
63
|
+
if not ef_path.exists() and not _is_ignored(ef_path, base_dir, ign):
|
|
64
|
+
result.add(
|
|
65
|
+
IssueSeverity.WARNING, "compose",
|
|
66
|
+
f"Service '{svc_name}' env_file missing: {ef}",
|
|
67
|
+
suggestion=f"Create {ef_path} or remove env_file entry.",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _check_volumes(
|
|
72
|
+
svc_name: str,
|
|
73
|
+
svc: dict,
|
|
74
|
+
base_dir: Path,
|
|
75
|
+
result: AnalysisResult,
|
|
76
|
+
ign: IgnoreList,
|
|
77
|
+
) -> None:
|
|
78
|
+
for vol in svc.get("volumes", []):
|
|
79
|
+
if not isinstance(vol, str) or ":" not in vol:
|
|
80
|
+
continue
|
|
81
|
+
host_part = vol.split(":", 1)[0]
|
|
82
|
+
if host_part.startswith("/"):
|
|
83
|
+
if not Path(host_part).exists() and not _is_ignored(Path(host_part), base_dir, ign):
|
|
84
|
+
result.add(
|
|
85
|
+
IssueSeverity.WARNING, "compose",
|
|
86
|
+
f"Service '{svc_name}' volume host path missing: {host_part}",
|
|
87
|
+
suggestion="Ensure host path exists before deployment.",
|
|
88
|
+
)
|
|
89
|
+
elif host_part != "." and "/" in host_part:
|
|
90
|
+
hp = base_dir / host_part
|
|
91
|
+
if not hp.exists() and not _is_ignored(hp, base_dir, ign):
|
|
92
|
+
result.add(
|
|
93
|
+
IssueSeverity.WARNING, "compose",
|
|
94
|
+
f"Service '{svc_name}' relative volume path missing: {hp}",
|
|
95
|
+
suggestion=f"Create {hp} or mount as named volume.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def scan_compose_file(
|
|
100
|
+
path: Path,
|
|
101
|
+
base_dir: Path,
|
|
102
|
+
result: AnalysisResult,
|
|
103
|
+
ign: IgnoreList,
|
|
104
|
+
) -> None:
|
|
105
|
+
try:
|
|
106
|
+
data = yaml.safe_load(path.read_text()) or {}
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
result.add(IssueSeverity.ERROR, "compose", f"Cannot parse {path}: {exc}")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
for svc_name, svc in (data.get("services") or {}).items():
|
|
112
|
+
if not isinstance(svc, dict):
|
|
113
|
+
continue
|
|
114
|
+
_check_build_section(svc_name, svc, base_dir, result, ign)
|
|
115
|
+
_check_env_files(svc_name, svc, base_dir, result, ign)
|
|
116
|
+
_check_volumes(svc_name, svc, base_dir, result, ign)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ComposeChecker(Checker):
|
|
120
|
+
"""Validate docker-compose files declared in spec or found in project."""
|
|
121
|
+
|
|
122
|
+
def check(self, spec, document, base_dir, result):
|
|
123
|
+
ign = IgnoreList(base_dir)
|
|
124
|
+
compose_files = list(spec.target.compose_files or [])
|
|
125
|
+
if not compose_files:
|
|
126
|
+
for candidate in (
|
|
127
|
+
"docker-compose.yml", "docker-compose.yaml",
|
|
128
|
+
"compose.yml", "compose.yaml",
|
|
129
|
+
):
|
|
130
|
+
if (base_dir / candidate).exists():
|
|
131
|
+
compose_files.append(candidate)
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
for cf in compose_files:
|
|
135
|
+
path = base_dir / cf
|
|
136
|
+
if not path.exists():
|
|
137
|
+
result.add(
|
|
138
|
+
IssueSeverity.ERROR, "compose",
|
|
139
|
+
f"Declared compose file not found: {cf}",
|
|
140
|
+
suggestion=f"Create {path} or remove from target.compose_files.",
|
|
141
|
+
)
|
|
142
|
+
continue
|
|
143
|
+
scan_compose_file(path, base_dir, result, ign)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Docker build command static validation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..models import AnalysisResult, IssueSeverity
|
|
8
|
+
from .base import Checker
|
|
9
|
+
|
|
10
|
+
DOCKER_BUILD_RE = re.compile(r"docker\s+(?:buildx\s+)?build\s+")
|
|
11
|
+
FILE_FLAG_RE = re.compile(r"-f\s+(\S+)|--file[=\s]+(\S+)")
|
|
12
|
+
STANDARD_DOCKERFILES = frozenset({
|
|
13
|
+
"Dockerfile", "dockerfile", "Dockerfile.prod", "Dockerfile.dev",
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def collect_sync_mappings(spec) -> tuple[set[str], list[tuple[str, str]]]:
|
|
18
|
+
sync_dests: set[str] = set()
|
|
19
|
+
sync_mappings: list[tuple[str, str]] = []
|
|
20
|
+
for step in spec.extra_steps:
|
|
21
|
+
sid = step.get("id", "")
|
|
22
|
+
if not str(sid).startswith("sync_"):
|
|
23
|
+
continue
|
|
24
|
+
dst = step.get("dst", "")
|
|
25
|
+
src = step.get("src", "")
|
|
26
|
+
if dst:
|
|
27
|
+
sync_dests.add(dst.rstrip("/"))
|
|
28
|
+
if src:
|
|
29
|
+
sync_mappings.append((dst.rstrip("/"), src))
|
|
30
|
+
return sync_dests, sync_mappings
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_docker_build(cmd: str) -> tuple[str | None, str]:
|
|
34
|
+
dockerfile = None
|
|
35
|
+
for match in FILE_FLAG_RE.finditer(cmd):
|
|
36
|
+
dockerfile = match.group(1) or match.group(2)
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
tokens = cmd.split()
|
|
40
|
+
context = "."
|
|
41
|
+
i = len(tokens) - 1
|
|
42
|
+
while i > 0:
|
|
43
|
+
token = tokens[i]
|
|
44
|
+
if token.startswith("-"):
|
|
45
|
+
i -= 1
|
|
46
|
+
continue
|
|
47
|
+
if i > 0 and tokens[i - 1] in ("-f", "--file"):
|
|
48
|
+
i -= 2
|
|
49
|
+
continue
|
|
50
|
+
if i > 0 and tokens[i - 1].startswith("--file="):
|
|
51
|
+
i -= 1
|
|
52
|
+
continue
|
|
53
|
+
context = token
|
|
54
|
+
break
|
|
55
|
+
i -= 1
|
|
56
|
+
return dockerfile, context
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def match_local_src(
|
|
60
|
+
sync_mappings: list[tuple[str, str]], remote_context: str,
|
|
61
|
+
) -> str | None:
|
|
62
|
+
for remote_dst, local_src in sync_mappings:
|
|
63
|
+
if not remote_dst.startswith("~/"):
|
|
64
|
+
continue
|
|
65
|
+
dst_clean = remote_dst.rstrip("/")
|
|
66
|
+
if remote_context == dst_clean or remote_context.startswith(dst_clean + "/"):
|
|
67
|
+
return local_src
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_local_dockerfile(local_src: str, dockerfile: str, base_dir: Path) -> Path | None:
|
|
72
|
+
src_path = Path(local_src.rstrip("/"))
|
|
73
|
+
if not src_path.is_absolute():
|
|
74
|
+
src_path = (base_dir / src_path).resolve()
|
|
75
|
+
if dockerfile.startswith("/"):
|
|
76
|
+
return Path(dockerfile)
|
|
77
|
+
return src_path / dockerfile
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DockerBuildChecker(Checker):
|
|
81
|
+
"""Validate docker build commands: Dockerfile exists, context is consistent."""
|
|
82
|
+
|
|
83
|
+
def check(self, spec, document, base_dir, result):
|
|
84
|
+
sync_dests, sync_mappings = collect_sync_mappings(spec)
|
|
85
|
+
|
|
86
|
+
for step in spec.extra_steps:
|
|
87
|
+
cmd = step.get("command", "")
|
|
88
|
+
if not DOCKER_BUILD_RE.search(cmd):
|
|
89
|
+
continue
|
|
90
|
+
sid = step.get("id", "unknown")
|
|
91
|
+
dockerfile, context = parse_docker_build(cmd)
|
|
92
|
+
self._check_dockerfile(dockerfile, sid, base_dir, result)
|
|
93
|
+
if context.startswith("~/"):
|
|
94
|
+
self._check_remote_context(
|
|
95
|
+
context, dockerfile, sid, sync_dests, sync_mappings,
|
|
96
|
+
base_dir, result,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _check_dockerfile(
|
|
100
|
+
self, dockerfile: str | None, sid: str, base_dir: Path, result: AnalysisResult,
|
|
101
|
+
) -> None:
|
|
102
|
+
if not dockerfile or dockerfile.startswith("/") or dockerfile.startswith("~"):
|
|
103
|
+
return
|
|
104
|
+
df_path = base_dir / dockerfile
|
|
105
|
+
if not df_path.exists():
|
|
106
|
+
result.add(
|
|
107
|
+
IssueSeverity.ERROR, "docker_build",
|
|
108
|
+
f"Step '{sid}' docker build references missing Dockerfile: {dockerfile}",
|
|
109
|
+
sid,
|
|
110
|
+
suggestion=f"Create {df_path} or use standard 'Dockerfile' name.",
|
|
111
|
+
)
|
|
112
|
+
elif dockerfile not in STANDARD_DOCKERFILES:
|
|
113
|
+
result.add(
|
|
114
|
+
IssueSeverity.WARNING, "docker_build",
|
|
115
|
+
f"Step '{sid}' uses non-standard Dockerfile name: {dockerfile}",
|
|
116
|
+
sid,
|
|
117
|
+
suggestion="Consider using standard 'Dockerfile' with multi-arch support.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _check_remote_context(
|
|
121
|
+
self,
|
|
122
|
+
context: str,
|
|
123
|
+
dockerfile: str | None,
|
|
124
|
+
sid: str,
|
|
125
|
+
sync_dests: set[str],
|
|
126
|
+
sync_mappings: list[tuple[str, str]],
|
|
127
|
+
base_dir: Path,
|
|
128
|
+
result: AnalysisResult,
|
|
129
|
+
) -> None:
|
|
130
|
+
context_clean = context.rstrip("/")
|
|
131
|
+
context_remote = context_clean[2:]
|
|
132
|
+
sync_remote_paths = {
|
|
133
|
+
d[2:].rstrip("/") for d in sync_dests if d.startswith("~/")
|
|
134
|
+
}
|
|
135
|
+
matched = any(
|
|
136
|
+
context_remote == sp or context_remote.startswith(sp + "/")
|
|
137
|
+
for sp in sync_remote_paths
|
|
138
|
+
)
|
|
139
|
+
if not matched and sync_dests:
|
|
140
|
+
result.add(
|
|
141
|
+
IssueSeverity.WARNING, "docker_build",
|
|
142
|
+
f"Step '{sid}' docker build uses remote context '{context}' "
|
|
143
|
+
f"that doesn't match any sync destination",
|
|
144
|
+
sid,
|
|
145
|
+
suggestion=f"Sync destinations: {', '.join(sorted(sync_dests))} — verify context path.",
|
|
146
|
+
)
|
|
147
|
+
if not dockerfile or not matched:
|
|
148
|
+
return
|
|
149
|
+
local_src = match_local_src(sync_mappings, context_clean)
|
|
150
|
+
if not local_src:
|
|
151
|
+
return
|
|
152
|
+
local_df = resolve_local_dockerfile(local_src, dockerfile, base_dir)
|
|
153
|
+
if local_df is not None and not local_df.exists():
|
|
154
|
+
result.add(
|
|
155
|
+
IssueSeverity.ERROR,
|
|
156
|
+
"docker_build",
|
|
157
|
+
f"Step '{sid}' Dockerfile '{dockerfile}' not found in synced local source '{local_src}'",
|
|
158
|
+
sid,
|
|
159
|
+
suggestion=f"Ensure {dockerfile} exists under {local_src} before sync/build.",
|
|
160
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Environment file checkers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..models import AnalysisResult, IssueSeverity
|
|
7
|
+
from .base import Checker
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EnvFileChecker(Checker):
|
|
11
|
+
"""Check that .env referenced by target.env_file exists."""
|
|
12
|
+
|
|
13
|
+
def check(self, spec, document, base_dir, result):
|
|
14
|
+
ef = spec.target.env_file or spec.source.env_file
|
|
15
|
+
if not ef:
|
|
16
|
+
return
|
|
17
|
+
path = base_dir / ef if not ef.startswith("/") else Path(ef)
|
|
18
|
+
if not path.exists():
|
|
19
|
+
result.add(
|
|
20
|
+
IssueSeverity.ERROR, "env",
|
|
21
|
+
f"env_file not found: {ef}",
|
|
22
|
+
suggestion=f"Create {path} or correct env_file path.",
|
|
23
|
+
)
|