redeploy 0.2.8__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.
- {redeploy-0.2.8 → redeploy-0.2.10}/PKG-INFO +6 -6
- {redeploy-0.2.8 → redeploy-0.2.10}/README.md +5 -5
- {redeploy-0.2.8 → redeploy-0.2.10}/pyproject.toml +1 -1
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/__init__.py +1 -1
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/cli.py +65 -20
- redeploy-0.2.10/redeploy/spec_loader.py +41 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_cli.py +34 -0
- redeploy-0.2.10/redeploy/tests/test_spec_loader.py +44 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_version_cli.py +26 -4
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy.egg-info/SOURCES.txt +2 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/discovery.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/fleet.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/models.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/observe.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/parse.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/patterns.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/ssh.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/steps.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/verify.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy/version.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/setup.cfg +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/tests/test_iac.py +0 -0
- {redeploy-0.2.8 → redeploy-0.2.10}/tests/test_parse.py +0 -0
- {redeploy-0.2.8 → 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.
|
|
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
|
-
    
|
|
30
|
+
  
|
|
31
31
|
|
|
32
|
-
- 🤖 **LLM usage:** $4.
|
|
33
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $4.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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.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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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]")
|
|
@@ -3177,18 +3187,53 @@ def _print_version_scan_review(console, root_sources, packages):
|
|
|
3177
3187
|
console.print(" No version sources detected")
|
|
3178
3188
|
|
|
3179
3189
|
|
|
3180
|
-
def
|
|
3190
|
+
def _classify_version_scan_source_confidence(source, *, actual: str) -> str:
|
|
3191
|
+
if actual == "(unreadable)":
|
|
3192
|
+
return "unreadable"
|
|
3193
|
+
if source.format == "regex":
|
|
3194
|
+
return "heuristic"
|
|
3195
|
+
return "certain"
|
|
3196
|
+
|
|
3197
|
+
|
|
3198
|
+
def _format_version_scan_source_status(confidence: str, *, conflict: bool) -> str:
|
|
3199
|
+
parts = [f"confidence={confidence}"]
|
|
3200
|
+
if conflict:
|
|
3201
|
+
parts.append("conflict=yes")
|
|
3202
|
+
return " ".join(parts)
|
|
3203
|
+
|
|
3204
|
+
|
|
3205
|
+
def _default_keep_scanned_source(confidence: str, *, conflict: bool) -> bool:
|
|
3206
|
+
return confidence == "certain" and not conflict
|
|
3207
|
+
|
|
3208
|
+
|
|
3209
|
+
def _summarize_version_scan_group(sources, *, default_version: str):
|
|
3181
3210
|
detected = _read_detected_source_versions(sources)
|
|
3182
3211
|
detected_map = {str(source.path): version for source, version in detected}
|
|
3183
3212
|
unique_versions = {version for _, version in detected}
|
|
3184
3213
|
chosen_version = detected[0][1] if detected else default_version
|
|
3185
|
-
|
|
3214
|
+
has_conflict = len(unique_versions) > 1
|
|
3215
|
+
reviewed_sources = []
|
|
3186
3216
|
|
|
3187
|
-
console.print(f" {label}: chosen version {chosen_version}{suffix}")
|
|
3188
3217
|
for source in sources:
|
|
3189
3218
|
actual = detected_map.get(str(source.path), "(unreadable)")
|
|
3190
|
-
|
|
3191
|
-
|
|
3219
|
+
confidence = _classify_version_scan_source_confidence(source, actual=actual)
|
|
3220
|
+
is_conflict = has_conflict and actual not in {"(unreadable)", chosen_version}
|
|
3221
|
+
reviewed_sources.append((source, actual, confidence, is_conflict))
|
|
3222
|
+
|
|
3223
|
+
return chosen_version, has_conflict, reviewed_sources
|
|
3224
|
+
|
|
3225
|
+
|
|
3226
|
+
def _print_version_scan_group(console, label: str, sources, *, default_version: str):
|
|
3227
|
+
chosen_version, has_conflict, reviewed_sources = _summarize_version_scan_group(
|
|
3228
|
+
sources,
|
|
3229
|
+
default_version=default_version,
|
|
3230
|
+
)
|
|
3231
|
+
suffix = " (conflict)" if has_conflict else ""
|
|
3232
|
+
|
|
3233
|
+
console.print(f" {label}: chosen version {chosen_version}{suffix}")
|
|
3234
|
+
for source, actual, confidence, is_conflict in reviewed_sources:
|
|
3235
|
+
status = _format_version_scan_source_status(confidence, conflict=is_conflict)
|
|
3236
|
+
console.print(f" - {source.path} ({source.format}) current: {actual} {status}")
|
|
3192
3237
|
|
|
3193
3238
|
|
|
3194
3239
|
def _iter_version_scan_groups(root_sources, packages):
|
|
@@ -3205,18 +3250,18 @@ def _review_detected_sources_interactively(console, root_sources, packages) -> s
|
|
|
3205
3250
|
console.print("[bold]Interactive scan review[/bold]")
|
|
3206
3251
|
|
|
3207
3252
|
for label, sources, default_version in _iter_version_scan_groups(root_sources, packages):
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
suffix = " (conflict)" if
|
|
3253
|
+
chosen_version, has_conflict, reviewed_sources = _summarize_version_scan_group(
|
|
3254
|
+
sources,
|
|
3255
|
+
default_version=default_version,
|
|
3256
|
+
)
|
|
3257
|
+
suffix = " (conflict)" if has_conflict else ""
|
|
3213
3258
|
|
|
3214
3259
|
console.print(f"\n[bold]{label}[/bold]: chosen version {chosen_version}{suffix}")
|
|
3215
|
-
for source in
|
|
3216
|
-
|
|
3260
|
+
for source, actual, confidence, is_conflict in reviewed_sources:
|
|
3261
|
+
status = _format_version_scan_source_status(confidence, conflict=is_conflict)
|
|
3217
3262
|
keep = click.confirm(
|
|
3218
|
-
f"Keep {source.path} ({source.format}) current={actual}?",
|
|
3219
|
-
default=
|
|
3263
|
+
f"Keep {source.path} ({source.format}) current={actual} {status}?",
|
|
3264
|
+
default=_default_keep_scanned_source(confidence, conflict=is_conflict),
|
|
3220
3265
|
)
|
|
3221
3266
|
if not keep:
|
|
3222
3267
|
rejected.add(source.path.as_posix())
|
|
@@ -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)
|
|
@@ -361,8 +361,9 @@ class TestVersionInit:
|
|
|
361
361
|
assert result.exit_code == 0, result.output
|
|
362
362
|
assert "Scan review" in result.output
|
|
363
363
|
assert "backend: chosen version 1.0.0 (conflict)" in result.output
|
|
364
|
-
assert "backend/VERSION (plain) current: 1.0.0" in result.output
|
|
365
|
-
assert "backend/src/version.ts (regex) current: 1.1.0
|
|
364
|
+
assert "backend/VERSION (plain) current: 1.0.0 confidence=certain" in result.output
|
|
365
|
+
assert "backend/src/version.ts (regex) current: 1.1.0 confidence=heuristic" in result.output
|
|
366
|
+
assert "conflict=yes" in result.output
|
|
366
367
|
assert "Review only - manifest not written" in result.output
|
|
367
368
|
assert not Path(".redeploy/version.yaml").exists()
|
|
368
369
|
|
|
@@ -385,8 +386,8 @@ class TestVersionInit:
|
|
|
385
386
|
|
|
386
387
|
assert result.exit_code == 0, result.output
|
|
387
388
|
assert "Interactive scan review" in result.output
|
|
388
|
-
assert "Keep backend/VERSION (plain) current=1.0.0?" in result.output
|
|
389
|
-
assert "Keep backend/src/version.ts (regex) current=1.1.0?" in result.output
|
|
389
|
+
assert "Keep backend/VERSION (plain) current=1.0.0 confidence=certain? [Y/n]:" in result.output
|
|
390
|
+
assert "Keep backend/src/version.ts (regex) current=1.1.0 confidence=heuristic conflict=yes? [y/N]:" in result.output
|
|
390
391
|
assert "Write manifest to .redeploy/version.yaml?" in result.output
|
|
391
392
|
|
|
392
393
|
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
@@ -395,6 +396,27 @@ class TestVersionInit:
|
|
|
395
396
|
{"path": "backend/VERSION", "format": "plain", "optional": False}
|
|
396
397
|
]
|
|
397
398
|
|
|
399
|
+
def test_init_scan_interactive_rejects_heuristic_source_by_default(self, tmp_path):
|
|
400
|
+
runner = _runner()
|
|
401
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
402
|
+
Path("packages/frontend/src").mkdir(parents=True, exist_ok=True)
|
|
403
|
+
Path("packages/frontend/src/version.ts").write_text(
|
|
404
|
+
'export const VERSION = "2.1.0"\n',
|
|
405
|
+
encoding="utf-8",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
result = runner.invoke(
|
|
409
|
+
cli,
|
|
410
|
+
["version", "init", "--scan", "--interactive"],
|
|
411
|
+
input="\n",
|
|
412
|
+
catch_exceptions=False,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
assert result.exit_code == 0, result.output
|
|
416
|
+
assert "Keep packages/frontend/src/version.ts (regex) current=2.1.0 confidence=heuristic? [y/N]:" in result.output
|
|
417
|
+
assert "No sources selected - manifest not written" in result.output
|
|
418
|
+
assert not Path(".redeploy/version.yaml").exists()
|
|
419
|
+
|
|
398
420
|
def test_init_interactive_requires_scan(self, tmp_path):
|
|
399
421
|
runner = _runner()
|
|
400
422
|
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
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
|
-
    
|
|
30
|
+
  
|
|
31
31
|
|
|
32
|
-
- 🤖 **LLM usage:** $4.
|
|
33
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|