redeploy 0.2.9__tar.gz → 0.2.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. {redeploy-0.2.9 → redeploy-0.2.10}/PKG-INFO +6 -6
  2. {redeploy-0.2.9 → redeploy-0.2.10}/README.md +5 -5
  3. {redeploy-0.2.9 → redeploy-0.2.10}/pyproject.toml +1 -1
  4. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/cli.py +16 -6
  6. redeploy-0.2.10/redeploy/spec_loader.py +41 -0
  7. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_cli.py +34 -0
  8. redeploy-0.2.10/redeploy/tests/test_spec_loader.py +44 -0
  9. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy.egg-info/PKG-INFO +6 -6
  10. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy.egg-info/SOURCES.txt +2 -0
  11. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/apply/__init__.py +0 -0
  12. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/apply/executor.py +0 -0
  13. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/data_sync.py +0 -0
  14. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/detect/__init__.py +0 -0
  15. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/detect/detector.py +0 -0
  16. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/detect/probes.py +0 -0
  17. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/detect/remote.py +0 -0
  18. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/detect/templates.py +0 -0
  19. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/detect/workflow.py +0 -0
  20. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/discovery.py +0 -0
  21. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl/__init__.py +0 -0
  22. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl/loader.py +0 -0
  23. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl/parser.py +0 -0
  24. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl_python/__init__.py +0 -0
  25. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl_python/context.py +0 -0
  26. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl_python/decorators.py +0 -0
  27. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl_python/docker_steps.py +0 -0
  28. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl_python/exceptions.py +0 -0
  29. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl_python/runner.py +0 -0
  30. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/dsl_python/steps.py +0 -0
  31. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/fleet.py +0 -0
  32. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/iac/__init__.py +0 -0
  33. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/iac/base.py +0 -0
  34. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/iac/docker_compose.py +0 -0
  35. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/iac/parsers/__init__.py +0 -0
  36. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/iac/parsers/compose.py +0 -0
  37. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/iac/registry.py +0 -0
  38. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/models.py +0 -0
  39. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/observe.py +0 -0
  40. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/parse.py +0 -0
  41. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/patterns.py +0 -0
  42. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/plan/__init__.py +0 -0
  43. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/plan/planner.py +0 -0
  44. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/plugins/__init__.py +0 -0
  45. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/plugins/builtin/__init__.py +0 -0
  46. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/plugins/builtin/browser_reload.py +0 -0
  47. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/plugins/builtin/notify.py +0 -0
  48. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  49. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/ssh.py +0 -0
  50. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/steps.py +0 -0
  51. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/__init__.py +0 -0
  52. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_data_sync.py +0 -0
  53. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_discovery.py +0 -0
  54. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_dsl.py +0 -0
  55. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_examples.py +0 -0
  56. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_executor.py +0 -0
  57. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_fleet.py +0 -0
  58. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_git_integration.py +0 -0
  59. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_iac.py +0 -0
  60. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_manifest.py +0 -0
  61. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_models.py +0 -0
  62. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_observe.py +0 -0
  63. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_parse.py +0 -0
  64. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_patterns.py +0 -0
  65. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_planner_edge.py +0 -0
  66. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_plugins.py +0 -0
  67. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_probes.py +0 -0
  68. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_public_api.py +0 -0
  69. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_ssh.py +0 -0
  70. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_templates.py +0 -0
  71. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_verify.py +0 -0
  72. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_version.py +0 -0
  73. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_version_changelog.py +0 -0
  74. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_version_cli.py +0 -0
  75. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_version_manifest.py +0 -0
  76. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_version_sources.py +0 -0
  77. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/tests/test_workflow.py +0 -0
  78. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/verify.py +0 -0
  79. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/__init__.py +0 -0
  80. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/bump.py +0 -0
  81. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/changelog.py +0 -0
  82. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/commits.py +0 -0
  83. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/diff.py +0 -0
  84. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/git_integration.py +0 -0
  85. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/git_transaction.py +0 -0
  86. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/manifest.py +0 -0
  87. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/sources/__init__.py +0 -0
  88. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/sources/base.py +0 -0
  89. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/sources/json_.py +0 -0
  90. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/sources/plain.py +0 -0
  91. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/sources/regex.py +0 -0
  92. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/sources/toml_.py +0 -0
  93. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/sources/yaml_.py +0 -0
  94. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version/transaction.py +0 -0
  95. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy/version.py +0 -0
  96. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy.egg-info/dependency_links.txt +0 -0
  97. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy.egg-info/entry_points.txt +0 -0
  98. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy.egg-info/requires.txt +0 -0
  99. {redeploy-0.2.9 → redeploy-0.2.10}/redeploy.egg-info/top_level.txt +0 -0
  100. {redeploy-0.2.9 → redeploy-0.2.10}/setup.cfg +0 -0
  101. {redeploy-0.2.9 → redeploy-0.2.10}/tests/test_iac.py +0 -0
  102. {redeploy-0.2.9 → redeploy-0.2.10}/tests/test_parse.py +0 -0
  103. {redeploy-0.2.9 → redeploy-0.2.10}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.9
3
+ Version: 0.2.10
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -26,17 +26,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
26
26
 
27
27
  ## AI Cost Tracking
28
28
 
29
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
30
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
29
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.10-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
30
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
31
 
32
- - 🤖 **LLM usage:** $4.2000 (28 commits)
33
- - 👤 **Human dev:** ~$802 (8.0h @ $100/h, 30min dedup)
32
+ - 🤖 **LLM usage:** $4.3500 (29 commits)
33
+ - 👤 **Human dev:** ~$866 (8.7h @ $100/h, 30min dedup)
34
34
 
35
35
  Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
36
36
 
37
37
  ---
38
38
 
39
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
39
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.10-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
40
 
41
41
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
42
42
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.10-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $4.2000 (28 commits)
10
- - 👤 **Human dev:** ~$802 (8.0h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $4.3500 (29 commits)
10
+ - 👤 **Human dev:** ~$866 (8.7h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.10-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.9"
7
+ version = "0.2.10"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
7
7
 
8
8
  Everything not listed in ``__all__`` is internal and may change without notice.
9
9
  """
10
- __version__ = "0.2.9"
10
+ __version__ = "0.2.10"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -853,7 +853,7 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output, env_nam
853
853
  redeploy run migration.yaml --detect --plan-out plan.yaml
854
854
  """
855
855
  from rich.console import Console
856
- from .models import MigrationSpec, ProjectManifest
856
+ from .models import ProjectManifest
857
857
  from .plan import Planner
858
858
 
859
859
  console = Console()
@@ -868,7 +868,7 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output, env_nam
868
868
  console.print("[dim] Create one with: redeploy init[/dim]")
869
869
  sys.exit(1)
870
870
 
871
- spec = MigrationSpec.from_file(resolved_spec)
871
+ spec = _load_spec_or_exit(console, resolved_spec)
872
872
 
873
873
  # overlay manifest values — env-specific or global
874
874
  if manifest:
@@ -937,6 +937,16 @@ def _find_manifest_path() -> str:
937
937
  return "redeploy.yaml"
938
938
 
939
939
 
940
+ def _load_spec_or_exit(console, path: str | Path):
941
+ from .spec_loader import SpecLoaderError, load_migration_spec
942
+
943
+ try:
944
+ return load_migration_spec(path)
945
+ except SpecLoaderError as exc:
946
+ console.print(f"[red]✗ {exc}[/red]")
947
+ sys.exit(1)
948
+
949
+
940
950
  def _resolve_device(console, device_id: str) -> tuple:
941
951
  """Resolve device from registry or auto-probe. Returns (device, registry) or (None, None)."""
942
952
  from .discovery import auto_probe
@@ -964,7 +974,7 @@ def _resolve_device(console, device_id: str) -> tuple:
964
974
 
965
975
  def _load_spec_with_manifest(console, spec_file: str | None, dev) -> "MigrationSpec":
966
976
  """Load spec and apply manifest/device overlays."""
967
- from .models import MigrationSpec, ProjectManifest
977
+ from .models import ProjectManifest
968
978
 
969
979
  manifest = ProjectManifest.find_and_load(Path.cwd())
970
980
  resolved_spec = spec_file or (manifest.spec if manifest else "migration.yaml")
@@ -972,7 +982,7 @@ def _load_spec_with_manifest(console, spec_file: str | None, dev) -> "MigrationS
972
982
  console.print(f"[red]✗ spec not found: {resolved_spec}[/red]")
973
983
  sys.exit(1)
974
984
 
975
- spec = MigrationSpec.from_file(resolved_spec)
985
+ spec = _load_spec_or_exit(console, resolved_spec)
976
986
  if manifest:
977
987
  manifest.apply_to_spec(spec)
978
988
 
@@ -1145,7 +1155,7 @@ def status(spec_file):
1145
1155
  """
1146
1156
  from rich.console import Console
1147
1157
  from rich.table import Table
1148
- from .models import MigrationSpec, ProjectManifest
1158
+ from .models import ProjectManifest
1149
1159
 
1150
1160
  console = Console()
1151
1161
 
@@ -1163,7 +1173,7 @@ def status(spec_file):
1163
1173
  resolved = spec_file or (manifest.spec if manifest else "migration.yaml")
1164
1174
  spec_path = Path(resolved)
1165
1175
  if spec_path.exists():
1166
- spec = MigrationSpec.from_file(str(spec_path))
1176
+ spec = _load_spec_or_exit(console, spec_path)
1167
1177
  if manifest:
1168
1178
  manifest.apply_to_spec(spec)
1169
1179
  console.print(f"\n[bold]{spec_path}[/bold] [dim]{spec.name}[/dim]")
@@ -0,0 +1,41 @@
1
+ """Load deployment specs from supported file formats."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from .models import MigrationSpec
7
+
8
+
9
+ class SpecLoaderError(ValueError):
10
+ """Base error raised when a deployment spec cannot be loaded."""
11
+
12
+
13
+ class UnsupportedSpecFormatError(SpecLoaderError):
14
+ """Raised when the spec file uses an unsupported format."""
15
+
16
+
17
+ def load_migration_spec(path: str | Path) -> MigrationSpec:
18
+ """Load a deployment spec from disk.
19
+
20
+ Supported today:
21
+ - `.yaml`
22
+ - `.yml`
23
+
24
+ Reserved for future support:
25
+ - `.md` markpact/markdown specs
26
+ """
27
+ spec_path = Path(path)
28
+ suffix = spec_path.suffix.lower()
29
+
30
+ if suffix in {"", ".yaml", ".yml"}:
31
+ return MigrationSpec.from_file(spec_path)
32
+
33
+ if suffix == ".md":
34
+ raise UnsupportedSpecFormatError(
35
+ "Unsupported spec format '.md': markdown/markpact specs are not implemented yet. "
36
+ "Use YAML (.yaml or .yml)."
37
+ )
38
+
39
+ raise UnsupportedSpecFormatError(
40
+ f"Unsupported spec format '{suffix}': use YAML (.yaml or .yml)."
41
+ )
@@ -101,6 +101,16 @@ class TestRunPlanOnly:
101
101
  result = _runner().invoke(cli, ["run", str(spec), "--plan-only", "--env", "prod"])
102
102
  assert result.exit_code == 0
103
103
 
104
+ def test_run_plan_only_rejects_markdown_spec_with_clear_error(self, tmp_path):
105
+ spec = tmp_path / "migration.md"
106
+ spec.write_text("# markpact prototype\n", encoding="utf-8")
107
+
108
+ result = _runner().invoke(cli, ["run", str(spec), "--plan-only"])
109
+
110
+ assert result.exit_code == 1
111
+ assert "Unsupported spec format '.md'" in result.output
112
+ assert "markdown/markpact specs are not implemented" in result.output
113
+
104
114
 
105
115
  # ── redeploy status ───────────────────────────────────────────────────────────
106
116
 
@@ -123,6 +133,17 @@ class TestStatus:
123
133
  assert result.exit_code == 0
124
134
  assert "myapp" in result.output
125
135
 
136
+ def test_status_rejects_markdown_spec_with_clear_error(self, tmp_path):
137
+ spec = tmp_path / "migration.md"
138
+ spec.write_text("# markpact prototype\n", encoding="utf-8")
139
+
140
+ with patch("redeploy.models.ProjectManifest.find_and_load", return_value=None):
141
+ result = _runner().invoke(cli, ["status", str(spec)])
142
+
143
+ assert result.exit_code == 1
144
+ assert "Unsupported spec format '.md'" in result.output
145
+ assert "markdown/markpact specs are not implemented" in result.output
146
+
126
147
 
127
148
  # ── redeploy init ─────────────────────────────────────────────────────────────
128
149
 
@@ -192,6 +213,19 @@ class TestDevices:
192
213
  assert data[0]["id"] == "x@1"
193
214
 
194
215
 
216
+ class TestTarget:
217
+ def test_target_rejects_markdown_spec_with_clear_error(self, tmp_path):
218
+ spec = tmp_path / "migration.md"
219
+ spec.write_text("# markpact prototype\n", encoding="utf-8")
220
+
221
+ with patch("redeploy.cli._resolve_device", return_value=(None, None)):
222
+ result = _runner().invoke(cli, ["target", "pi@192.168.1.42", str(spec), "--plan-only"])
223
+
224
+ assert result.exit_code == 1
225
+ assert "Unsupported spec format '.md'" in result.output
226
+ assert "markdown/markpact specs are not implemented" in result.output
227
+
228
+
195
229
  # ── redeploy apply ────────────────────────────────────────────────────────────
196
230
 
197
231
 
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import textwrap
4
+
5
+ import pytest
6
+
7
+ from redeploy.spec_loader import UnsupportedSpecFormatError, load_migration_spec
8
+
9
+
10
+ def _migration_yaml() -> str:
11
+ return textwrap.dedent("""\
12
+ name: loader test
13
+ source:
14
+ strategy: docker_full
15
+ host: local
16
+ app: myapp
17
+ version: "1.0.0"
18
+ target:
19
+ strategy: docker_full
20
+ host: local
21
+ app: myapp
22
+ version: "1.0.1"
23
+ remote_dir: ~/myapp
24
+ """)
25
+
26
+
27
+ def test_load_migration_spec_reads_yaml(tmp_path):
28
+ spec_path = tmp_path / "migration.yaml"
29
+ spec_path.write_text(_migration_yaml(), encoding="utf-8")
30
+
31
+ spec = load_migration_spec(spec_path)
32
+
33
+ assert spec.name == "loader test"
34
+ assert spec.target.version == "1.0.1"
35
+
36
+
37
+ def test_load_migration_spec_rejects_markdown_for_now(tmp_path):
38
+ spec_path = tmp_path / "migration.md"
39
+ spec_path.write_text("# prototype\n", encoding="utf-8")
40
+
41
+ with pytest.raises(UnsupportedSpecFormatError) as exc_info:
42
+ load_migration_spec(spec_path)
43
+
44
+ assert "markdown/markpact specs are not implemented yet" in str(exc_info.value)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.9
3
+ Version: 0.2.10
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -26,17 +26,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
26
26
 
27
27
  ## AI Cost Tracking
28
28
 
29
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
30
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
29
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.10-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
30
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.35-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
31
31
 
32
- - 🤖 **LLM usage:** $4.2000 (28 commits)
33
- - 👤 **Human dev:** ~$802 (8.0h @ $100/h, 30min dedup)
32
+ - 🤖 **LLM usage:** $4.3500 (29 commits)
33
+ - 👤 **Human dev:** ~$866 (8.7h @ $100/h, 30min dedup)
34
34
 
35
35
  Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
36
36
 
37
37
  ---
38
38
 
39
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.9-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
39
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.10-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
40
 
41
41
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
42
42
 
@@ -9,6 +9,7 @@ redeploy/models.py
9
9
  redeploy/observe.py
10
10
  redeploy/parse.py
11
11
  redeploy/patterns.py
12
+ redeploy/spec_loader.py
12
13
  redeploy/ssh.py
13
14
  redeploy/steps.py
14
15
  redeploy/verify.py
@@ -69,6 +70,7 @@ redeploy/tests/test_planner_edge.py
69
70
  redeploy/tests/test_plugins.py
70
71
  redeploy/tests/test_probes.py
71
72
  redeploy/tests/test_public_api.py
73
+ redeploy/tests/test_spec_loader.py
72
74
  redeploy/tests/test_ssh.py
73
75
  redeploy/tests/test_templates.py
74
76
  redeploy/tests/test_verify.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes