redeploy 0.2.1__tar.gz → 0.2.4__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.1 → redeploy-0.2.4}/PKG-INFO +57 -7
- {redeploy-0.2.1 → redeploy-0.2.4}/README.md +56 -6
- {redeploy-0.2.1 → redeploy-0.2.4}/pyproject.toml +1 -1
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/__init__.py +52 -1
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/apply/executor.py +54 -7
- redeploy-0.2.4/redeploy/cli.py +2551 -0
- redeploy-0.2.4/redeploy/dsl/__init__.py +30 -0
- redeploy-0.2.4/redeploy/dsl/loader.py +403 -0
- redeploy-0.2.4/redeploy/dsl/parser.py +261 -0
- redeploy-0.2.4/redeploy/iac/__init__.py +47 -0
- redeploy-0.2.4/redeploy/iac/base.py +275 -0
- redeploy-0.2.4/redeploy/iac/docker_compose.py +418 -0
- redeploy-0.2.4/redeploy/iac/parsers/__init__.py +1 -0
- redeploy-0.2.4/redeploy/iac/parsers/compose.py +287 -0
- redeploy-0.2.4/redeploy/iac/registry.py +37 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/models.py +43 -1
- redeploy-0.2.4/redeploy/observe.py +313 -0
- redeploy-0.2.4/redeploy/plugins/__init__.py +180 -0
- redeploy-0.2.4/redeploy/plugins/builtin/__init__.py +1 -0
- redeploy-0.2.4/redeploy/plugins/builtin/browser_reload.py +120 -0
- redeploy-0.2.4/redeploy/plugins/builtin/notify.py +168 -0
- redeploy-0.2.4/redeploy/plugins/builtin/systemd_reload.py +98 -0
- redeploy-0.2.4/redeploy/tests/test_dsl.py +354 -0
- redeploy-0.2.4/redeploy/tests/test_git_integration.py +325 -0
- redeploy-0.2.4/redeploy/tests/test_iac.py +461 -0
- redeploy-0.2.4/redeploy/tests/test_observe.py +369 -0
- redeploy-0.2.4/redeploy/tests/test_plugins.py +410 -0
- redeploy-0.2.4/redeploy/tests/test_version_changelog.py +356 -0
- redeploy-0.2.4/redeploy/tests/test_version_cli.py +172 -0
- redeploy-0.2.4/redeploy/tests/test_version_manifest.py +658 -0
- redeploy-0.2.4/redeploy/tests/test_version_sources.py +517 -0
- redeploy-0.2.4/redeploy/version/__init__.py +148 -0
- redeploy-0.2.4/redeploy/version/bump.py +284 -0
- redeploy-0.2.4/redeploy/version/changelog.py +195 -0
- redeploy-0.2.4/redeploy/version/commits.py +218 -0
- redeploy-0.2.4/redeploy/version/diff.py +118 -0
- redeploy-0.2.4/redeploy/version/git_integration.py +142 -0
- redeploy-0.2.4/redeploy/version/git_transaction.py +105 -0
- redeploy-0.2.4/redeploy/version/manifest.py +169 -0
- redeploy-0.2.4/redeploy/version/sources/__init__.py +65 -0
- redeploy-0.2.4/redeploy/version/sources/base.py +44 -0
- redeploy-0.2.4/redeploy/version/sources/json_.py +94 -0
- redeploy-0.2.4/redeploy/version/sources/plain.py +28 -0
- redeploy-0.2.4/redeploy/version/sources/regex.py +69 -0
- redeploy-0.2.4/redeploy/version/sources/toml_.py +108 -0
- redeploy-0.2.4/redeploy/version/sources/yaml_.py +98 -0
- redeploy-0.2.4/redeploy/version/transaction.py +162 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy.egg-info/PKG-INFO +57 -7
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy.egg-info/SOURCES.txt +41 -0
- redeploy-0.2.4/tests/test_iac.py +511 -0
- redeploy-0.2.1/redeploy/cli.py +0 -1188
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/discovery.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/fleet.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/parse.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/patterns.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/ssh.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/steps.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/verify.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy/version.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/setup.cfg +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/tests/test_parse.py +0 -0
- {redeploy-0.2.1 → redeploy-0.2.4}/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.4
|
|
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:** $3.3000 (22 commits)
|
|
33
|
+
- 👤 **Human dev:** ~$533 (5.3h @ $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
|
|
|
@@ -350,7 +350,57 @@ extra_steps:
|
|
|
350
350
|
insert_before: docker_build_pull # runs before build, not after verify
|
|
351
351
|
```
|
|
352
352
|
|
|
353
|
-
##
|
|
353
|
+
## Plugin system
|
|
354
|
+
|
|
355
|
+
Extend the step pipeline with custom action types using `action: plugin`:
|
|
356
|
+
|
|
357
|
+
```yaml
|
|
358
|
+
extra_steps:
|
|
359
|
+
- id: reload_kiosk
|
|
360
|
+
action: plugin
|
|
361
|
+
plugin_type: browser_reload
|
|
362
|
+
description: Reload kiosk browser after deploy
|
|
363
|
+
plugin_params:
|
|
364
|
+
port: 9222
|
|
365
|
+
ignore_cache: true
|
|
366
|
+
url_contains: "localhost:8100"
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Built-in plugins
|
|
370
|
+
|
|
371
|
+
| `plugin_type` | Description | `plugin_params` |
|
|
372
|
+
|---------------|-------------|-----------------|
|
|
373
|
+
| `browser_reload` | Reload Chromium via CDP (Chrome DevTools Protocol) over SSH | `port` (9222), `ignore_cache` (true), `url_contains` ("") |
|
|
374
|
+
|
|
375
|
+
### Writing a custom plugin
|
|
376
|
+
|
|
377
|
+
Place a `.py` file in `./redeploy_plugins/` (project-local) or `~/.redeploy/plugins/` (user-global):
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
# ./redeploy_plugins/notify.py
|
|
381
|
+
from redeploy.plugins import register_plugin, PluginContext
|
|
382
|
+
from redeploy.models import StepStatus
|
|
383
|
+
|
|
384
|
+
@register_plugin("notify_slack")
|
|
385
|
+
def notify_slack(ctx: PluginContext) -> None:
|
|
386
|
+
webhook = ctx.params["webhook"]
|
|
387
|
+
ctx.probe.run(f"curl -X POST {webhook} -d '{{\"text\":\"deployed!\"}}'")
|
|
388
|
+
ctx.step.result = "notified"
|
|
389
|
+
ctx.step.status = StepStatus.DONE
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
`PluginContext` fields:
|
|
393
|
+
|
|
394
|
+
| Field | Type | Description |
|
|
395
|
+
|-------|------|-------------|
|
|
396
|
+
| `step` | `MigrationStep` | Current step — set `result` and `status` here |
|
|
397
|
+
| `host` | `str` | SSH host (e.g. `pi@192.168.1.5`) |
|
|
398
|
+
| `probe` | `RemoteProbe` | Call `probe.run(cmd)` for remote SSH commands |
|
|
399
|
+
| `emitter` | `ProgressEmitter?` | Emit mid-step progress: `emitter.progress(step.id, msg)` |
|
|
400
|
+
| `params` | `dict` | Shortcut for `step.plugin_params` |
|
|
401
|
+
| `dry_run` | `bool` | Skip side-effects if True |
|
|
402
|
+
|
|
403
|
+
|
|
354
404
|
|
|
355
405
|
Place in project root — `redeploy run` (no args) uses it automatically.
|
|
356
406
|
Supports **named environments** for multi-target projects:
|
|
@@ -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:** $3.3000 (22 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$533 (5.3h @ $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
|
|
|
@@ -327,7 +327,57 @@ extra_steps:
|
|
|
327
327
|
insert_before: docker_build_pull # runs before build, not after verify
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
##
|
|
330
|
+
## Plugin system
|
|
331
|
+
|
|
332
|
+
Extend the step pipeline with custom action types using `action: plugin`:
|
|
333
|
+
|
|
334
|
+
```yaml
|
|
335
|
+
extra_steps:
|
|
336
|
+
- id: reload_kiosk
|
|
337
|
+
action: plugin
|
|
338
|
+
plugin_type: browser_reload
|
|
339
|
+
description: Reload kiosk browser after deploy
|
|
340
|
+
plugin_params:
|
|
341
|
+
port: 9222
|
|
342
|
+
ignore_cache: true
|
|
343
|
+
url_contains: "localhost:8100"
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Built-in plugins
|
|
347
|
+
|
|
348
|
+
| `plugin_type` | Description | `plugin_params` |
|
|
349
|
+
|---------------|-------------|-----------------|
|
|
350
|
+
| `browser_reload` | Reload Chromium via CDP (Chrome DevTools Protocol) over SSH | `port` (9222), `ignore_cache` (true), `url_contains` ("") |
|
|
351
|
+
|
|
352
|
+
### Writing a custom plugin
|
|
353
|
+
|
|
354
|
+
Place a `.py` file in `./redeploy_plugins/` (project-local) or `~/.redeploy/plugins/` (user-global):
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
# ./redeploy_plugins/notify.py
|
|
358
|
+
from redeploy.plugins import register_plugin, PluginContext
|
|
359
|
+
from redeploy.models import StepStatus
|
|
360
|
+
|
|
361
|
+
@register_plugin("notify_slack")
|
|
362
|
+
def notify_slack(ctx: PluginContext) -> None:
|
|
363
|
+
webhook = ctx.params["webhook"]
|
|
364
|
+
ctx.probe.run(f"curl -X POST {webhook} -d '{{\"text\":\"deployed!\"}}'")
|
|
365
|
+
ctx.step.result = "notified"
|
|
366
|
+
ctx.step.status = StepStatus.DONE
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`PluginContext` fields:
|
|
370
|
+
|
|
371
|
+
| Field | Type | Description |
|
|
372
|
+
|-------|------|-------------|
|
|
373
|
+
| `step` | `MigrationStep` | Current step — set `result` and `status` here |
|
|
374
|
+
| `host` | `str` | SSH host (e.g. `pi@192.168.1.5`) |
|
|
375
|
+
| `probe` | `RemoteProbe` | Call `probe.run(cmd)` for remote SSH commands |
|
|
376
|
+
| `emitter` | `ProgressEmitter?` | Emit mid-step progress: `emitter.progress(step.id, msg)` |
|
|
377
|
+
| `params` | `dict` | Shortcut for `step.plugin_params` |
|
|
378
|
+
| `dry_run` | `bool` | Skip side-effects if True |
|
|
379
|
+
|
|
380
|
+
|
|
331
381
|
|
|
332
382
|
Place in project root — `redeploy run` (no args) uses it automatically.
|
|
333
383
|
Supports **named environments** for multi-target projects:
|
|
@@ -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.4"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -55,6 +55,36 @@ from .patterns import ( # noqa: F401
|
|
|
55
55
|
pattern_registry,
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
+
# ── Observability (first-class from 0.2.0) ────────────────────────────────────
|
|
59
|
+
from .observe import ( # noqa: F401
|
|
60
|
+
AuditEntry,
|
|
61
|
+
DeployAuditLog,
|
|
62
|
+
DeployReport,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# ── IaC parsers (first-class from 0.3.0) ──────────────────────────────────────
|
|
66
|
+
from .iac import ( # noqa: F401
|
|
67
|
+
ParsedSpec,
|
|
68
|
+
Parser,
|
|
69
|
+
ParserRegistry,
|
|
70
|
+
PortInfo,
|
|
71
|
+
ServiceInfo,
|
|
72
|
+
VolumeInfo,
|
|
73
|
+
ConversionWarning,
|
|
74
|
+
parse_file,
|
|
75
|
+
parse_dir,
|
|
76
|
+
parser_registry,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# ── Plugin system (first-class from 0.2.2) ────────────────────────────────────
|
|
80
|
+
from .plugins import ( # noqa: F401
|
|
81
|
+
PluginContext,
|
|
82
|
+
PluginRegistry,
|
|
83
|
+
register_plugin,
|
|
84
|
+
load_user_plugins,
|
|
85
|
+
registry as plugin_registry,
|
|
86
|
+
)
|
|
87
|
+
|
|
58
88
|
__all__ = [
|
|
59
89
|
# version
|
|
60
90
|
"__version__",
|
|
@@ -95,4 +125,25 @@ __all__ = [
|
|
|
95
125
|
"get_pattern",
|
|
96
126
|
"list_patterns",
|
|
97
127
|
"pattern_registry",
|
|
128
|
+
# observability
|
|
129
|
+
"AuditEntry",
|
|
130
|
+
"DeployAuditLog",
|
|
131
|
+
"DeployReport",
|
|
132
|
+
# iac parsers
|
|
133
|
+
"ParsedSpec",
|
|
134
|
+
"Parser",
|
|
135
|
+
"ParserRegistry",
|
|
136
|
+
"PortInfo",
|
|
137
|
+
"ServiceInfo",
|
|
138
|
+
"VolumeInfo",
|
|
139
|
+
"ConversionWarning",
|
|
140
|
+
"parse_file",
|
|
141
|
+
"parse_dir",
|
|
142
|
+
"parser_registry",
|
|
143
|
+
# plugin system
|
|
144
|
+
"PluginContext",
|
|
145
|
+
"PluginRegistry",
|
|
146
|
+
"register_plugin",
|
|
147
|
+
"load_user_plugins",
|
|
148
|
+
"plugin_registry",
|
|
98
149
|
]
|
|
@@ -14,6 +14,7 @@ from loguru import logger
|
|
|
14
14
|
|
|
15
15
|
from ..detect.remote import RemoteProbe
|
|
16
16
|
from ..models import MigrationPlan, MigrationStep, StepAction, StepStatus
|
|
17
|
+
from ..plugins import PluginContext, registry as _plugin_registry
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class ProgressEmitter:
|
|
@@ -121,7 +122,9 @@ class Executor:
|
|
|
121
122
|
def __init__(self, plan: MigrationPlan, dry_run: bool = False,
|
|
122
123
|
ssh_key: Optional[str] = None,
|
|
123
124
|
progress_yaml: bool = False,
|
|
124
|
-
progress_stream: IO[str] = None
|
|
125
|
+
progress_stream: IO[str] = None,
|
|
126
|
+
audit_log: bool = True,
|
|
127
|
+
audit_path: Optional["Path"] = None):
|
|
125
128
|
self.plan = plan
|
|
126
129
|
self.dry_run = dry_run
|
|
127
130
|
self.probe = RemoteProbe(plan.host)
|
|
@@ -131,15 +134,24 @@ class Executor:
|
|
|
131
134
|
self._emitter: Optional[ProgressEmitter] = (
|
|
132
135
|
ProgressEmitter(progress_stream) if progress_yaml else None
|
|
133
136
|
)
|
|
137
|
+
self._audit_log = audit_log
|
|
138
|
+
self._audit_path = audit_path
|
|
139
|
+
self._t0: float = 0.0
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def completed_steps(self) -> list[MigrationStep]:
|
|
143
|
+
return list(self._completed)
|
|
134
144
|
|
|
135
145
|
def run(self) -> bool:
|
|
136
146
|
"""Execute all steps. Returns True if all passed."""
|
|
147
|
+
self._t0 = time.monotonic()
|
|
137
148
|
prefix = "[DRY RUN] " if self.dry_run else ""
|
|
138
149
|
logger.info(f"{prefix}Applying plan: {len(self.plan.steps)} steps "
|
|
139
150
|
f"({self.plan.from_strategy.value} → {self.plan.to_strategy.value})")
|
|
140
151
|
if self._emitter:
|
|
141
152
|
self._emitter.start(self.plan)
|
|
142
153
|
|
|
154
|
+
ok = True
|
|
143
155
|
for i, step in enumerate(self.plan.steps, 1):
|
|
144
156
|
try:
|
|
145
157
|
if self._emitter:
|
|
@@ -157,12 +169,27 @@ class Executor:
|
|
|
157
169
|
self._emitter.failed(len(self._completed), len(self.plan.steps), str(e))
|
|
158
170
|
if not self.dry_run:
|
|
159
171
|
self._rollback()
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
ok = False
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
elapsed = time.monotonic() - self._t0
|
|
176
|
+
if ok:
|
|
177
|
+
logger.info(f"{'[DRY RUN] ' if self.dry_run else ''}All {len(self.plan.steps)} steps completed")
|
|
178
|
+
if self._emitter:
|
|
179
|
+
self._emitter.done(len(self.plan.steps))
|
|
180
|
+
self._write_audit(ok=ok, elapsed_s=elapsed)
|
|
181
|
+
return ok
|
|
182
|
+
|
|
183
|
+
def _write_audit(self, *, ok: bool, elapsed_s: float) -> None:
|
|
184
|
+
if not self._audit_log:
|
|
185
|
+
return
|
|
186
|
+
try:
|
|
187
|
+
from ..observe import DeployAuditLog
|
|
188
|
+
log = DeployAuditLog(path=self._audit_path)
|
|
189
|
+
log.record(self.plan, self._completed, ok=ok,
|
|
190
|
+
elapsed_s=elapsed_s, dry_run=self.dry_run)
|
|
191
|
+
except Exception as exc: # never crash the executor
|
|
192
|
+
logger.debug(f"audit_log write failed (non-fatal): {exc}")
|
|
166
193
|
|
|
167
194
|
# ── step dispatcher ───────────────────────────────────────────────────────
|
|
168
195
|
|
|
@@ -191,6 +218,7 @@ class Executor:
|
|
|
191
218
|
StepAction.HTTP_CHECK: self._run_http_check,
|
|
192
219
|
StepAction.VERSION_CHECK: self._run_version_check,
|
|
193
220
|
StepAction.WAIT: self._run_wait,
|
|
221
|
+
StepAction.PLUGIN: self._run_plugin,
|
|
194
222
|
}
|
|
195
223
|
|
|
196
224
|
handler = dispatch.get(step.action)
|
|
@@ -391,6 +419,25 @@ class Executor:
|
|
|
391
419
|
step.status = StepStatus.DONE
|
|
392
420
|
step.result = f"version {step.expect} confirmed"
|
|
393
421
|
|
|
422
|
+
def _run_plugin(self, step: MigrationStep) -> None:
|
|
423
|
+
"""Dispatch to a registered plugin handler."""
|
|
424
|
+
plugin_type = step.plugin_type
|
|
425
|
+
if not plugin_type:
|
|
426
|
+
raise StepError(step, "plugin action requires plugin_type field")
|
|
427
|
+
handler = _plugin_registry.get(plugin_type)
|
|
428
|
+
if not handler:
|
|
429
|
+
available = ", ".join(_plugin_registry.names()) or "(none loaded)"
|
|
430
|
+
raise StepError(step, f"unknown plugin_type '{plugin_type}'. Available: {available}")
|
|
431
|
+
ctx = PluginContext(
|
|
432
|
+
step=step,
|
|
433
|
+
host=self.plan.host,
|
|
434
|
+
probe=self.probe,
|
|
435
|
+
emitter=self._emitter,
|
|
436
|
+
params=step.plugin_params,
|
|
437
|
+
dry_run=self.dry_run,
|
|
438
|
+
)
|
|
439
|
+
handler(ctx)
|
|
440
|
+
|
|
394
441
|
def _run_wait(self, step: MigrationStep) -> None:
|
|
395
442
|
total = step.seconds
|
|
396
443
|
if total <= 0:
|