redeploy 0.2.7__tar.gz → 0.2.8__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.7 → redeploy-0.2.8}/PKG-INFO +6 -6
- {redeploy-0.2.7 → redeploy-0.2.8}/README.md +5 -5
- {redeploy-0.2.7 → redeploy-0.2.8}/pyproject.toml +1 -1
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/__init__.py +1 -1
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/cli.py +181 -20
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_version_cli.py +146 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/discovery.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/fleet.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/models.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/observe.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/parse.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/patterns.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/ssh.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/steps.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/verify.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy/version.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy.egg-info/SOURCES.txt +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/setup.cfg +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/tests/test_iac.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/tests/test_parse.py +0 -0
- {redeploy-0.2.7 → redeploy-0.2.8}/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.8
|
|
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:** $
|
|
33
|
-
- 👤 **Human dev:** ~$
|
|
32
|
+
- 🤖 **LLM usage:** $4.0500 (27 commits)
|
|
33
|
+
- 👤 **Human dev:** ~$773 (7.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:** $
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $4.0500 (27 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$773 (7.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.8"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -2953,8 +2953,13 @@ def version_set(version, manifest_path_str, package_name, all_packages, dry_run,
|
|
|
2953
2953
|
|
|
2954
2954
|
@version_cmd.command(name="init")
|
|
2955
2955
|
@click.option("--scan", is_flag=True, help="Auto-detect version sources")
|
|
2956
|
+
@click.option("--review", is_flag=True, help="Review detected sources without writing manifest")
|
|
2957
|
+
@click.option("--interactive", is_flag=True,
|
|
2958
|
+
help="Interactively accept or reject detected sources before writing manifest")
|
|
2959
|
+
@click.option("--exclude", "excluded_paths", multiple=True,
|
|
2960
|
+
help="Exclude a detected source path from scan results (repeatable)")
|
|
2956
2961
|
@click.option("--force", is_flag=True, help="Overwrite existing manifest")
|
|
2957
|
-
def version_init(scan, force):
|
|
2962
|
+
def version_init(scan, review, interactive, excluded_paths, force):
|
|
2958
2963
|
"""Initialize .redeploy/version.yaml manifest."""
|
|
2959
2964
|
from rich.console import Console
|
|
2960
2965
|
from .version.manifest import VersionManifest, SourceConfig, GitConfig
|
|
@@ -2972,10 +2977,36 @@ def version_init(scan, force):
|
|
|
2972
2977
|
root = Path.cwd()
|
|
2973
2978
|
root_sources = []
|
|
2974
2979
|
packages = None
|
|
2980
|
+
excluded = _normalize_scan_exclusions(excluded_paths)
|
|
2981
|
+
|
|
2982
|
+
if (review or interactive or excluded) and not scan:
|
|
2983
|
+
console.print("[red]✗ --review, --interactive and --exclude require --scan[/red]")
|
|
2984
|
+
sys.exit(1)
|
|
2975
2985
|
|
|
2976
2986
|
if scan:
|
|
2977
|
-
root_sources = _detect_version_sources_in_dir(root, root)
|
|
2978
|
-
packages = _scan_package_version_manifests(root)
|
|
2987
|
+
root_sources = _detect_version_sources_in_dir(root, root, excluded_paths=excluded)
|
|
2988
|
+
packages = _scan_package_version_manifests(root, excluded_paths=excluded)
|
|
2989
|
+
|
|
2990
|
+
if interactive:
|
|
2991
|
+
excluded.update(
|
|
2992
|
+
_review_detected_sources_interactively(console, root_sources, packages)
|
|
2993
|
+
)
|
|
2994
|
+
root_sources = _detect_version_sources_in_dir(root, root, excluded_paths=excluded)
|
|
2995
|
+
packages = _scan_package_version_manifests(root, excluded_paths=excluded)
|
|
2996
|
+
|
|
2997
|
+
if not root_sources and not packages:
|
|
2998
|
+
console.print("[yellow]⚠ No sources selected - manifest not written[/yellow]")
|
|
2999
|
+
return
|
|
3000
|
+
|
|
3001
|
+
_print_version_scan_review(console, root_sources, packages)
|
|
3002
|
+
if not click.confirm("Write manifest to .redeploy/version.yaml?", default=True):
|
|
3003
|
+
console.print("[yellow]Review complete - manifest not written[/yellow]")
|
|
3004
|
+
return
|
|
3005
|
+
|
|
3006
|
+
if review and not interactive:
|
|
3007
|
+
_print_version_scan_review(console, root_sources, packages)
|
|
3008
|
+
console.print("[yellow]Review only - manifest not written[/yellow]")
|
|
3009
|
+
return
|
|
2979
3010
|
|
|
2980
3011
|
if packages:
|
|
2981
3012
|
current = _detect_source_version(root_sources, default="0.0.0") if root_sources else "0.0.0"
|
|
@@ -3017,6 +3048,14 @@ def version_init(scan, force):
|
|
|
3017
3048
|
package = m.get_package(package_name)
|
|
3018
3049
|
console.print(f" - {package_name}: {package.version} ({len(package.sources)} sources)")
|
|
3019
3050
|
|
|
3051
|
+
if scan:
|
|
3052
|
+
if m.sources:
|
|
3053
|
+
_report_version_scan_conflict(console, "root", m.sources, m.version)
|
|
3054
|
+
if m.is_monorepo():
|
|
3055
|
+
for package_name in m.list_packages():
|
|
3056
|
+
package = m.get_package(package_name)
|
|
3057
|
+
_report_version_scan_conflict(console, package_name, package.sources, package.version)
|
|
3058
|
+
|
|
3020
3059
|
|
|
3021
3060
|
def _version_scan_specs():
|
|
3022
3061
|
return [
|
|
@@ -3026,14 +3065,35 @@ def _version_scan_specs():
|
|
|
3026
3065
|
]
|
|
3027
3066
|
|
|
3028
3067
|
|
|
3068
|
+
def _regex_version_scan_specs():
|
|
3069
|
+
return [
|
|
3070
|
+
("__init__.py", r'__version__\s*=\s*["\']([^"\']+)["\']'),
|
|
3071
|
+
("src/__init__.py", r'__version__\s*=\s*["\']([^"\']+)["\']'),
|
|
3072
|
+
("version.ts", r'(?:export\s+)?(?:const|let|var)\s+VERSION\s*=\s*["\']([^"\']+)["\']'),
|
|
3073
|
+
("src/version.ts", r'(?:export\s+)?(?:const|let|var)\s+VERSION\s*=\s*["\']([^"\']+)["\']'),
|
|
3074
|
+
("version.h", r'#define\s+[A-Z0-9_]*VERSION\s+"([^"]+)"'),
|
|
3075
|
+
("src/version.h", r'#define\s+[A-Z0-9_]*VERSION\s+"([^"]+)"'),
|
|
3076
|
+
("include/version.h", r'#define\s+[A-Z0-9_]*VERSION\s+"([^"]+)"'),
|
|
3077
|
+
]
|
|
3078
|
+
|
|
3079
|
+
|
|
3029
3080
|
def _is_scannable_version_path(path: Path) -> bool:
|
|
3030
3081
|
ignored = {".git", ".venv", ".redeploy", "node_modules", "__pycache__", "dist", "build"}
|
|
3031
3082
|
return not any(part in ignored for part in path.parts)
|
|
3032
3083
|
|
|
3033
3084
|
|
|
3034
|
-
def
|
|
3085
|
+
def _normalize_scan_exclusions(excluded_paths) -> set[str]:
|
|
3086
|
+
return {
|
|
3087
|
+
str(Path(path)).replace("\\", "/")
|
|
3088
|
+
for path in excluded_paths
|
|
3089
|
+
if str(path).strip()
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
|
|
3093
|
+
def _detect_version_sources_in_dir(directory: Path, workspace_root: Path, *, excluded_paths: set[str] | None = None):
|
|
3035
3094
|
from .version.manifest import SourceConfig
|
|
3036
3095
|
|
|
3096
|
+
excluded_paths = excluded_paths or set()
|
|
3037
3097
|
sources = []
|
|
3038
3098
|
for filename, format_name, key in _version_scan_specs():
|
|
3039
3099
|
candidate = directory / filename
|
|
@@ -3043,6 +3103,8 @@ def _detect_version_sources_in_dir(directory: Path, workspace_root: Path):
|
|
|
3043
3103
|
relative_path = candidate.relative_to(workspace_root)
|
|
3044
3104
|
if not _is_scannable_version_path(relative_path):
|
|
3045
3105
|
continue
|
|
3106
|
+
if relative_path.as_posix() in excluded_paths:
|
|
3107
|
+
continue
|
|
3046
3108
|
|
|
3047
3109
|
source_kwargs = {
|
|
3048
3110
|
"path": relative_path,
|
|
@@ -3052,18 +3114,114 @@ def _detect_version_sources_in_dir(directory: Path, workspace_root: Path):
|
|
|
3052
3114
|
source_kwargs["key"] = key
|
|
3053
3115
|
sources.append(SourceConfig(**source_kwargs))
|
|
3054
3116
|
|
|
3117
|
+
for relative_filename, pattern in _regex_version_scan_specs():
|
|
3118
|
+
candidate = directory / relative_filename
|
|
3119
|
+
if not candidate.exists() or not candidate.is_file():
|
|
3120
|
+
continue
|
|
3121
|
+
|
|
3122
|
+
relative_path = candidate.relative_to(workspace_root)
|
|
3123
|
+
if not _is_scannable_version_path(relative_path):
|
|
3124
|
+
continue
|
|
3125
|
+
if relative_path.as_posix() in excluded_paths:
|
|
3126
|
+
continue
|
|
3127
|
+
|
|
3128
|
+
sources.append(SourceConfig(
|
|
3129
|
+
path=relative_path,
|
|
3130
|
+
format="regex",
|
|
3131
|
+
pattern=pattern,
|
|
3132
|
+
))
|
|
3133
|
+
|
|
3055
3134
|
return sources
|
|
3056
3135
|
|
|
3057
3136
|
|
|
3058
3137
|
def _detect_source_version(sources, *, default: str):
|
|
3138
|
+
detected = _read_detected_source_versions(sources)
|
|
3139
|
+
return detected[0][1] if detected else default
|
|
3140
|
+
|
|
3141
|
+
|
|
3142
|
+
def _read_detected_source_versions(sources):
|
|
3059
3143
|
from .version.sources import get_adapter
|
|
3060
3144
|
|
|
3145
|
+
detected = []
|
|
3061
3146
|
for source in sources:
|
|
3062
3147
|
try:
|
|
3063
|
-
|
|
3148
|
+
detected.append((source, get_adapter(source.format).read(source.path, source)))
|
|
3064
3149
|
except Exception:
|
|
3065
3150
|
continue
|
|
3066
|
-
return
|
|
3151
|
+
return detected
|
|
3152
|
+
|
|
3153
|
+
|
|
3154
|
+
def _report_version_scan_conflict(console, label: str, sources, chosen_version: str):
|
|
3155
|
+
detected = _read_detected_source_versions(sources)
|
|
3156
|
+
unique_versions = {version for _, version in detected}
|
|
3157
|
+
if len(unique_versions) <= 1:
|
|
3158
|
+
return
|
|
3159
|
+
|
|
3160
|
+
details = ", ".join(f"{source.path}={version}" for source, version in detected)
|
|
3161
|
+
console.print(
|
|
3162
|
+
f"[yellow]⚠ Version conflict in {label}: {details}; using {chosen_version}[/yellow]"
|
|
3163
|
+
)
|
|
3164
|
+
|
|
3165
|
+
|
|
3166
|
+
def _print_version_scan_review(console, root_sources, packages):
|
|
3167
|
+
console.print("[bold]Scan review[/bold]")
|
|
3168
|
+
|
|
3169
|
+
if root_sources:
|
|
3170
|
+
_print_version_scan_group(console, "root", root_sources, default_version="0.0.0")
|
|
3171
|
+
|
|
3172
|
+
if packages:
|
|
3173
|
+
for package_name, package in packages.items():
|
|
3174
|
+
_print_version_scan_group(console, package_name, package.sources, default_version=package.version)
|
|
3175
|
+
|
|
3176
|
+
if not root_sources and not packages:
|
|
3177
|
+
console.print(" No version sources detected")
|
|
3178
|
+
|
|
3179
|
+
|
|
3180
|
+
def _print_version_scan_group(console, label: str, sources, *, default_version: str):
|
|
3181
|
+
detected = _read_detected_source_versions(sources)
|
|
3182
|
+
detected_map = {str(source.path): version for source, version in detected}
|
|
3183
|
+
unique_versions = {version for _, version in detected}
|
|
3184
|
+
chosen_version = detected[0][1] if detected else default_version
|
|
3185
|
+
suffix = " (conflict)" if len(unique_versions) > 1 else ""
|
|
3186
|
+
|
|
3187
|
+
console.print(f" {label}: chosen version {chosen_version}{suffix}")
|
|
3188
|
+
for source in sources:
|
|
3189
|
+
actual = detected_map.get(str(source.path), "(unreadable)")
|
|
3190
|
+
marker = " (conflict)" if len(unique_versions) > 1 and actual != chosen_version else ""
|
|
3191
|
+
console.print(f" - {source.path} ({source.format}) current: {actual}{marker}")
|
|
3192
|
+
|
|
3193
|
+
|
|
3194
|
+
def _iter_version_scan_groups(root_sources, packages):
|
|
3195
|
+
if root_sources:
|
|
3196
|
+
yield "root", root_sources, "0.0.0"
|
|
3197
|
+
|
|
3198
|
+
if packages:
|
|
3199
|
+
for package_name, package in packages.items():
|
|
3200
|
+
yield package_name, package.sources, package.version
|
|
3201
|
+
|
|
3202
|
+
|
|
3203
|
+
def _review_detected_sources_interactively(console, root_sources, packages) -> set[str]:
|
|
3204
|
+
rejected = set()
|
|
3205
|
+
console.print("[bold]Interactive scan review[/bold]")
|
|
3206
|
+
|
|
3207
|
+
for label, sources, default_version in _iter_version_scan_groups(root_sources, packages):
|
|
3208
|
+
detected = _read_detected_source_versions(sources)
|
|
3209
|
+
detected_map = {str(source.path): version for source, version in detected}
|
|
3210
|
+
unique_versions = {version for _, version in detected}
|
|
3211
|
+
chosen_version = detected[0][1] if detected else default_version
|
|
3212
|
+
suffix = " (conflict)" if len(unique_versions) > 1 else ""
|
|
3213
|
+
|
|
3214
|
+
console.print(f"\n[bold]{label}[/bold]: chosen version {chosen_version}{suffix}")
|
|
3215
|
+
for source in sources:
|
|
3216
|
+
actual = detected_map.get(str(source.path), "(unreadable)")
|
|
3217
|
+
keep = click.confirm(
|
|
3218
|
+
f"Keep {source.path} ({source.format}) current={actual}?",
|
|
3219
|
+
default=True,
|
|
3220
|
+
)
|
|
3221
|
+
if not keep:
|
|
3222
|
+
rejected.add(source.path.as_posix())
|
|
3223
|
+
|
|
3224
|
+
return rejected
|
|
3067
3225
|
|
|
3068
3226
|
|
|
3069
3227
|
def _derive_scanned_package_name(package_dir: Path, workspace_root: Path, used_names: set[str]) -> str:
|
|
@@ -3079,25 +3237,28 @@ def _derive_scanned_package_name(package_dir: Path, workspace_root: Path, used_n
|
|
|
3079
3237
|
return candidate
|
|
3080
3238
|
|
|
3081
3239
|
|
|
3082
|
-
def _scan_package_version_manifests(workspace_root: Path):
|
|
3240
|
+
def _scan_package_version_manifests(workspace_root: Path, *, excluded_paths: set[str] | None = None):
|
|
3083
3241
|
from .version.manifest import PackageConfig
|
|
3084
3242
|
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3243
|
+
excluded_paths = excluded_paths or set()
|
|
3244
|
+
package_dirs = []
|
|
3245
|
+
for candidate in sorted(workspace_root.rglob("*")):
|
|
3246
|
+
if not candidate.is_dir() or candidate == workspace_root:
|
|
3247
|
+
continue
|
|
3248
|
+
|
|
3249
|
+
relative_path = candidate.relative_to(workspace_root)
|
|
3250
|
+
if not _is_scannable_version_path(relative_path):
|
|
3251
|
+
continue
|
|
3252
|
+
if candidate.name in {"src", "include"}:
|
|
3253
|
+
continue
|
|
3254
|
+
|
|
3255
|
+
if _detect_version_sources_in_dir(candidate, workspace_root, excluded_paths=excluded_paths):
|
|
3256
|
+
package_dirs.append(candidate)
|
|
3096
3257
|
|
|
3097
3258
|
packages = {}
|
|
3098
3259
|
used_names = set()
|
|
3099
|
-
for package_dir in
|
|
3100
|
-
sources = _detect_version_sources_in_dir(package_dir, workspace_root)
|
|
3260
|
+
for package_dir in package_dirs:
|
|
3261
|
+
sources = _detect_version_sources_in_dir(package_dir, workspace_root, excluded_paths=excluded_paths)
|
|
3101
3262
|
if not sources:
|
|
3102
3263
|
continue
|
|
3103
3264
|
|
|
@@ -291,6 +291,152 @@ class TestVersionInit:
|
|
|
291
291
|
assert "Policy: independent" in result.output
|
|
292
292
|
assert "Packages: 2" in result.output
|
|
293
293
|
|
|
294
|
+
def test_init_scan_detects_regex_source_in_nested_package(self, tmp_path):
|
|
295
|
+
runner = _runner()
|
|
296
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
297
|
+
Path("packages/frontend/src").mkdir(parents=True, exist_ok=True)
|
|
298
|
+
Path("packages/frontend/src/version.ts").write_text(
|
|
299
|
+
'export const VERSION = "2.1.0"\n',
|
|
300
|
+
encoding="utf-8",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
result = runner.invoke(
|
|
304
|
+
cli,
|
|
305
|
+
["version", "init", "--scan"],
|
|
306
|
+
catch_exceptions=False,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
assert result.exit_code == 0, result.output
|
|
310
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
311
|
+
assert manifest["version"]["policy"] == "independent"
|
|
312
|
+
assert manifest["version"]["packages"]["frontend"]["version"] == "2.1.0"
|
|
313
|
+
source = manifest["version"]["packages"]["frontend"]["sources"][0]
|
|
314
|
+
assert source["path"] == "packages/frontend/src/version.ts"
|
|
315
|
+
assert source["format"] == "regex"
|
|
316
|
+
assert source["optional"] is False
|
|
317
|
+
assert "VERSION" in source["pattern"]
|
|
318
|
+
assert "const|let|var" in source["pattern"]
|
|
319
|
+
|
|
320
|
+
def test_init_scan_reports_conflict_between_scanned_sources(self, tmp_path):
|
|
321
|
+
runner = _runner()
|
|
322
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
323
|
+
Path("backend/src").mkdir(parents=True, exist_ok=True)
|
|
324
|
+
Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
|
|
325
|
+
Path("backend/src/version.ts").write_text(
|
|
326
|
+
'export const VERSION = "1.1.0"\n',
|
|
327
|
+
encoding="utf-8",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
result = runner.invoke(
|
|
331
|
+
cli,
|
|
332
|
+
["version", "init", "--scan"],
|
|
333
|
+
catch_exceptions=False,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
assert result.exit_code == 0, result.output
|
|
337
|
+
assert "Version conflict in backend" in result.output
|
|
338
|
+
assert "backend/VERSION=1.0.0" in result.output
|
|
339
|
+
assert "backend/src/version.ts=1.1.0" in result.output
|
|
340
|
+
assert "using 1.0.0" in result.output
|
|
341
|
+
|
|
342
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
343
|
+
assert manifest["version"]["packages"]["backend"]["version"] == "1.0.0"
|
|
344
|
+
|
|
345
|
+
def test_init_scan_review_lists_detected_sources_without_writing_manifest(self, tmp_path):
|
|
346
|
+
runner = _runner()
|
|
347
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
348
|
+
Path("backend/src").mkdir(parents=True, exist_ok=True)
|
|
349
|
+
Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
|
|
350
|
+
Path("backend/src/version.ts").write_text(
|
|
351
|
+
'export const VERSION = "1.1.0"\n',
|
|
352
|
+
encoding="utf-8",
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
result = runner.invoke(
|
|
356
|
+
cli,
|
|
357
|
+
["version", "init", "--scan", "--review"],
|
|
358
|
+
catch_exceptions=False,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
assert result.exit_code == 0, result.output
|
|
362
|
+
assert "Scan review" in result.output
|
|
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 (conflict)" in result.output
|
|
366
|
+
assert "Review only - manifest not written" in result.output
|
|
367
|
+
assert not Path(".redeploy/version.yaml").exists()
|
|
368
|
+
|
|
369
|
+
def test_init_scan_interactive_can_reject_source_and_write_manifest(self, tmp_path):
|
|
370
|
+
runner = _runner()
|
|
371
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
372
|
+
Path("backend/src").mkdir(parents=True, exist_ok=True)
|
|
373
|
+
Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
|
|
374
|
+
Path("backend/src/version.ts").write_text(
|
|
375
|
+
'export const VERSION = "1.1.0"\n',
|
|
376
|
+
encoding="utf-8",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
result = runner.invoke(
|
|
380
|
+
cli,
|
|
381
|
+
["version", "init", "--scan", "--interactive"],
|
|
382
|
+
input="\nn\n\n",
|
|
383
|
+
catch_exceptions=False,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
assert result.exit_code == 0, result.output
|
|
387
|
+
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
|
|
390
|
+
assert "Write manifest to .redeploy/version.yaml?" in result.output
|
|
391
|
+
|
|
392
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
393
|
+
assert manifest["version"]["packages"]["backend"]["version"] == "1.0.0"
|
|
394
|
+
assert manifest["version"]["packages"]["backend"]["sources"] == [
|
|
395
|
+
{"path": "backend/VERSION", "format": "plain", "optional": False}
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
def test_init_interactive_requires_scan(self, tmp_path):
|
|
399
|
+
runner = _runner()
|
|
400
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
401
|
+
result = runner.invoke(
|
|
402
|
+
cli,
|
|
403
|
+
["version", "init", "--interactive"],
|
|
404
|
+
catch_exceptions=False,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
assert result.exit_code == 1, result.output
|
|
408
|
+
assert "--review, --interactive and --exclude require --scan" in result.output
|
|
409
|
+
|
|
410
|
+
def test_init_scan_exclude_omits_detected_source_from_manifest(self, tmp_path):
|
|
411
|
+
runner = _runner()
|
|
412
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
413
|
+
Path("backend/src").mkdir(parents=True, exist_ok=True)
|
|
414
|
+
Path("backend/VERSION").write_text("1.0.0\n", encoding="utf-8")
|
|
415
|
+
Path("backend/src/version.ts").write_text(
|
|
416
|
+
'export const VERSION = "1.1.0"\n',
|
|
417
|
+
encoding="utf-8",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
result = runner.invoke(
|
|
421
|
+
cli,
|
|
422
|
+
[
|
|
423
|
+
"version",
|
|
424
|
+
"init",
|
|
425
|
+
"--scan",
|
|
426
|
+
"--exclude",
|
|
427
|
+
"backend/src/version.ts",
|
|
428
|
+
],
|
|
429
|
+
catch_exceptions=False,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
assert result.exit_code == 0, result.output
|
|
433
|
+
manifest = yaml.safe_load(Path(".redeploy/version.yaml").read_text(encoding="utf-8"))
|
|
434
|
+
assert manifest["version"]["packages"]["backend"]["version"] == "1.0.0"
|
|
435
|
+
assert manifest["version"]["packages"]["backend"]["sources"] == [
|
|
436
|
+
{"path": "backend/VERSION", "format": "plain", "optional": False}
|
|
437
|
+
]
|
|
438
|
+
assert "Version conflict in backend" not in result.output
|
|
439
|
+
|
|
294
440
|
|
|
295
441
|
class TestVersionVerify:
|
|
296
442
|
def test_verify_all_packages_succeeds_when_in_sync(self, tmp_path):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
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:** $
|
|
33
|
-
- 👤 **Human dev:** ~$
|
|
32
|
+
- 🤖 **LLM usage:** $4.0500 (27 commits)
|
|
33
|
+
- 👤 **Human dev:** ~$773 (7.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
|
|
|
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
|
|
File without changes
|
|
File without changes
|