redeploy 0.2.16__tar.gz → 0.2.18__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.16 → redeploy-0.2.18}/PKG-INFO +136 -7
- {redeploy-0.2.16 → redeploy-0.2.18}/README.md +135 -6
- {redeploy-0.2.16 → redeploy-0.2.18}/pyproject.toml +1 -1
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/__init__.py +1 -1
- redeploy-0.2.18/redeploy/apply/__init__.py +16 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/apply/executor.py +174 -1
- redeploy-0.2.18/redeploy/apply/state.py +161 -0
- redeploy-0.2.18/redeploy/audit.py +556 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/cli.py +448 -5
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/markpact/__init__.py +11 -1
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/markpact/compiler.py +7 -2
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/markpact/models.py +3 -1
- redeploy-0.2.18/redeploy/markpact/parser.py +255 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/models.py +4 -1
- redeploy-0.2.18/redeploy/tests/test_audit.py +244 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_executor.py +107 -0
- redeploy-0.2.18/redeploy/tests/test_markpact_parser.py +93 -0
- redeploy-0.2.18/redeploy/tests/test_resume.py +271 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy.egg-info/PKG-INFO +136 -7
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy.egg-info/SOURCES.txt +4 -0
- redeploy-0.2.16/redeploy/apply/__init__.py +0 -4
- redeploy-0.2.16/redeploy/markpact/parser.py +0 -72
- redeploy-0.2.16/redeploy/tests/test_markpact_parser.py +0 -41
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/discovery.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/fleet.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/observe.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/parse.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/patterns.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/ssh.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/steps.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/verify.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy/version.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/setup.cfg +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/tests/test_iac.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/tests/test_parse.py +0 -0
- {redeploy-0.2.16 → redeploy-0.2.18}/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.18
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -27,17 +27,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
|
27
27
|
|
|
28
28
|
## AI Cost Tracking
|
|
29
29
|
|
|
30
|
-
    
|
|
31
|
+
  
|
|
32
32
|
|
|
33
|
-
- 🤖 **LLM usage:** $5.
|
|
34
|
-
- 👤 **Human dev:** ~$
|
|
33
|
+
- 🤖 **LLM usage:** $5.5500 (37 commits)
|
|
34
|
+
- 👤 **Human dev:** ~$1255 (12.6h @ $100/h, 30min dedup)
|
|
35
35
|
|
|
36
|
-
Generated on 2026-04-
|
|
36
|
+
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
    
|
|
41
41
|
|
|
42
42
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
43
43
|
|
|
@@ -401,6 +401,135 @@ def notify_slack(ctx: PluginContext) -> None:
|
|
|
401
401
|
| `params` | `dict` | Shortcut for `step.plugin_params` |
|
|
402
402
|
| `dry_run` | `bool` | Skip side-effects if True |
|
|
403
403
|
|
|
404
|
+
## Inline Scripts
|
|
405
|
+
|
|
406
|
+
Execute multiline bash scripts directly from YAML without external files:
|
|
407
|
+
|
|
408
|
+
```yaml
|
|
409
|
+
extra_steps:
|
|
410
|
+
- id: configure_kiosk
|
|
411
|
+
action: inline_script
|
|
412
|
+
description: "Deploy kiosk launch script"
|
|
413
|
+
command: |
|
|
414
|
+
#!/bin/bash
|
|
415
|
+
mkdir -p ~/c2004/config
|
|
416
|
+
cat > ~/c2004/config/kiosk-launch.sh << 'EOF'
|
|
417
|
+
#!/bin/bash
|
|
418
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
419
|
+
chromium-browser --kiosk http://localhost:8100
|
|
420
|
+
elif command -v firefox >/dev/null 2>&1; then
|
|
421
|
+
firefox --kiosk http://localhost:8100
|
|
422
|
+
fi
|
|
423
|
+
EOF
|
|
424
|
+
chmod +x ~/c2004/config/kiosk-launch.sh
|
|
425
|
+
risk: medium
|
|
426
|
+
timeout: 60
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
The script is base64-encoded and executed via SSH with automatic temp file cleanup. Use `command` field for multiline script content (YAML `|` preserves newlines).
|
|
430
|
+
|
|
431
|
+
### Script References (`command_ref`)
|
|
432
|
+
|
|
433
|
+
Instead of duplicating scripts in YAML, reference a script defined in a markdown codeblock:
|
|
434
|
+
|
|
435
|
+
```yaml
|
|
436
|
+
extra_steps:
|
|
437
|
+
- id: configure_kiosk
|
|
438
|
+
action: inline_script
|
|
439
|
+
description: "Execute kiosk script from markdown"
|
|
440
|
+
command_ref: "#kiosk-browser-configuration-script"
|
|
441
|
+
risk: medium
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
In your migration markdown file, define the script in a section:
|
|
445
|
+
|
|
446
|
+
```markdown
|
|
447
|
+
## Kiosk Browser Configuration Script
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
#!/bin/bash
|
|
451
|
+
# Auto-detect browser...
|
|
452
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
453
|
+
chromium-browser --kiosk http://localhost:8100
|
|
454
|
+
fi
|
|
455
|
+
```
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Benefits:**
|
|
459
|
+
- Single source of truth — script lives in one place (markdown codeblock)
|
|
460
|
+
- No duplication between markdown documentation and YAML
|
|
461
|
+
- Easy to read and maintain
|
|
462
|
+
- Changes to the codeblock automatically apply to the deployment
|
|
463
|
+
|
|
464
|
+
**Reference formats:**
|
|
465
|
+
- `"#section-id"` — script from section in current spec file
|
|
466
|
+
- `"./file.md#section-id"` — script from section in specific file
|
|
467
|
+
|
|
468
|
+
The section ID is derived from the heading: spaces become hyphens, lowercase.
|
|
469
|
+
Example: `## Kiosk Browser Configuration Script` → `#kiosk-browser-configuration-script`
|
|
470
|
+
|
|
471
|
+
### Execute Script by Reference (`redeploy exec`)
|
|
472
|
+
|
|
473
|
+
Run a single script from markdown without running the full migration:
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
# Execute script from codeblock on remote host
|
|
477
|
+
redeploy exec '#kiosk-browser-configuration-script' \
|
|
478
|
+
--host pi@192.168.188.108 \
|
|
479
|
+
--file migration.podman-rpi5-resume.md
|
|
480
|
+
|
|
481
|
+
# With file in reference
|
|
482
|
+
redeploy exec './migration.md#install-deps' --host root@server.com
|
|
483
|
+
|
|
484
|
+
# Using markpact:ref (more explicit)
|
|
485
|
+
redeploy exec 'kiosk-script-id' --host pi@192.168.188.108 --file migration.md
|
|
486
|
+
|
|
487
|
+
# Dry-run to preview script
|
|
488
|
+
redeploy exec '#backup-script' --host pi@192.168.188.108 --file ops.md --dry-run
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
This is useful for:
|
|
492
|
+
- One-off operations defined in markdown docs
|
|
493
|
+
- Testing individual scripts before full migration
|
|
494
|
+
- Running maintenance tasks
|
|
495
|
+
|
|
496
|
+
### Execute Multiple Scripts (`redeploy exec-multi`)
|
|
497
|
+
|
|
498
|
+
Test multiple scripts at once:
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
# Execute multiple scripts by ref
|
|
502
|
+
redeploy exec-multi 'kiosk-script,install-deps,cleanup' \
|
|
503
|
+
--host pi@192.168.188.108 \
|
|
504
|
+
--file migration.md
|
|
505
|
+
|
|
506
|
+
# Mix of markpact:ref and section headings
|
|
507
|
+
redeploy exec-multi 'script1,#section2,script3' \
|
|
508
|
+
--host root@server.com \
|
|
509
|
+
--file deploy.md \
|
|
510
|
+
--dry-run
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Marking Codeblocks with `markpact:ref`
|
|
514
|
+
|
|
515
|
+
For more explicit script identification, use `markpact:ref <id>` in codeblock:
|
|
516
|
+
|
|
517
|
+
```markdown
|
|
518
|
+
```bash markpact:ref kiosk-browser-configuration-script
|
|
519
|
+
#!/bin/bash
|
|
520
|
+
# Auto-detect browser...
|
|
521
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
522
|
+
chromium-browser --kiosk http://localhost:8100
|
|
523
|
+
fi
|
|
524
|
+
```
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Benefits of `markpact:ref`:
|
|
528
|
+
- Explicit ID assignment (not derived from heading)
|
|
529
|
+
- Multiple scripts per section
|
|
530
|
+
- Can reference by simple ID instead of full heading
|
|
531
|
+
- Self-documenting in markdown
|
|
532
|
+
|
|
404
533
|
|
|
405
534
|
|
|
406
535
|
Place in project root — `redeploy run` (no args) uses it automatically.
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $5.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $5.5500 (37 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1255 (12.6h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-04-
|
|
12
|
+
Generated on 2026-04-21 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
|
|
|
@@ -377,6 +377,135 @@ def notify_slack(ctx: PluginContext) -> None:
|
|
|
377
377
|
| `params` | `dict` | Shortcut for `step.plugin_params` |
|
|
378
378
|
| `dry_run` | `bool` | Skip side-effects if True |
|
|
379
379
|
|
|
380
|
+
## Inline Scripts
|
|
381
|
+
|
|
382
|
+
Execute multiline bash scripts directly from YAML without external files:
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
extra_steps:
|
|
386
|
+
- id: configure_kiosk
|
|
387
|
+
action: inline_script
|
|
388
|
+
description: "Deploy kiosk launch script"
|
|
389
|
+
command: |
|
|
390
|
+
#!/bin/bash
|
|
391
|
+
mkdir -p ~/c2004/config
|
|
392
|
+
cat > ~/c2004/config/kiosk-launch.sh << 'EOF'
|
|
393
|
+
#!/bin/bash
|
|
394
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
395
|
+
chromium-browser --kiosk http://localhost:8100
|
|
396
|
+
elif command -v firefox >/dev/null 2>&1; then
|
|
397
|
+
firefox --kiosk http://localhost:8100
|
|
398
|
+
fi
|
|
399
|
+
EOF
|
|
400
|
+
chmod +x ~/c2004/config/kiosk-launch.sh
|
|
401
|
+
risk: medium
|
|
402
|
+
timeout: 60
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The script is base64-encoded and executed via SSH with automatic temp file cleanup. Use `command` field for multiline script content (YAML `|` preserves newlines).
|
|
406
|
+
|
|
407
|
+
### Script References (`command_ref`)
|
|
408
|
+
|
|
409
|
+
Instead of duplicating scripts in YAML, reference a script defined in a markdown codeblock:
|
|
410
|
+
|
|
411
|
+
```yaml
|
|
412
|
+
extra_steps:
|
|
413
|
+
- id: configure_kiosk
|
|
414
|
+
action: inline_script
|
|
415
|
+
description: "Execute kiosk script from markdown"
|
|
416
|
+
command_ref: "#kiosk-browser-configuration-script"
|
|
417
|
+
risk: medium
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
In your migration markdown file, define the script in a section:
|
|
421
|
+
|
|
422
|
+
```markdown
|
|
423
|
+
## Kiosk Browser Configuration Script
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
#!/bin/bash
|
|
427
|
+
# Auto-detect browser...
|
|
428
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
429
|
+
chromium-browser --kiosk http://localhost:8100
|
|
430
|
+
fi
|
|
431
|
+
```
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Benefits:**
|
|
435
|
+
- Single source of truth — script lives in one place (markdown codeblock)
|
|
436
|
+
- No duplication between markdown documentation and YAML
|
|
437
|
+
- Easy to read and maintain
|
|
438
|
+
- Changes to the codeblock automatically apply to the deployment
|
|
439
|
+
|
|
440
|
+
**Reference formats:**
|
|
441
|
+
- `"#section-id"` — script from section in current spec file
|
|
442
|
+
- `"./file.md#section-id"` — script from section in specific file
|
|
443
|
+
|
|
444
|
+
The section ID is derived from the heading: spaces become hyphens, lowercase.
|
|
445
|
+
Example: `## Kiosk Browser Configuration Script` → `#kiosk-browser-configuration-script`
|
|
446
|
+
|
|
447
|
+
### Execute Script by Reference (`redeploy exec`)
|
|
448
|
+
|
|
449
|
+
Run a single script from markdown without running the full migration:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
# Execute script from codeblock on remote host
|
|
453
|
+
redeploy exec '#kiosk-browser-configuration-script' \
|
|
454
|
+
--host pi@192.168.188.108 \
|
|
455
|
+
--file migration.podman-rpi5-resume.md
|
|
456
|
+
|
|
457
|
+
# With file in reference
|
|
458
|
+
redeploy exec './migration.md#install-deps' --host root@server.com
|
|
459
|
+
|
|
460
|
+
# Using markpact:ref (more explicit)
|
|
461
|
+
redeploy exec 'kiosk-script-id' --host pi@192.168.188.108 --file migration.md
|
|
462
|
+
|
|
463
|
+
# Dry-run to preview script
|
|
464
|
+
redeploy exec '#backup-script' --host pi@192.168.188.108 --file ops.md --dry-run
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
This is useful for:
|
|
468
|
+
- One-off operations defined in markdown docs
|
|
469
|
+
- Testing individual scripts before full migration
|
|
470
|
+
- Running maintenance tasks
|
|
471
|
+
|
|
472
|
+
### Execute Multiple Scripts (`redeploy exec-multi`)
|
|
473
|
+
|
|
474
|
+
Test multiple scripts at once:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# Execute multiple scripts by ref
|
|
478
|
+
redeploy exec-multi 'kiosk-script,install-deps,cleanup' \
|
|
479
|
+
--host pi@192.168.188.108 \
|
|
480
|
+
--file migration.md
|
|
481
|
+
|
|
482
|
+
# Mix of markpact:ref and section headings
|
|
483
|
+
redeploy exec-multi 'script1,#section2,script3' \
|
|
484
|
+
--host root@server.com \
|
|
485
|
+
--file deploy.md \
|
|
486
|
+
--dry-run
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Marking Codeblocks with `markpact:ref`
|
|
490
|
+
|
|
491
|
+
For more explicit script identification, use `markpact:ref <id>` in codeblock:
|
|
492
|
+
|
|
493
|
+
```markdown
|
|
494
|
+
```bash markpact:ref kiosk-browser-configuration-script
|
|
495
|
+
#!/bin/bash
|
|
496
|
+
# Auto-detect browser...
|
|
497
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
498
|
+
chromium-browser --kiosk http://localhost:8100
|
|
499
|
+
fi
|
|
500
|
+
```
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Benefits of `markpact:ref`:
|
|
504
|
+
- Explicit ID assignment (not derived from heading)
|
|
505
|
+
- Multiple scripts per section
|
|
506
|
+
- Can reference by simple ID instead of full heading
|
|
507
|
+
- Self-documenting in markdown
|
|
508
|
+
|
|
380
509
|
|
|
381
510
|
|
|
382
511
|
Place in project root — `redeploy run` (no args) uses it automatically.
|
|
@@ -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.18"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""apply — Execute a MigrationPlan step by step."""
|
|
2
|
+
from .executor import Executor
|
|
3
|
+
from .state import (
|
|
4
|
+
DEFAULT_STATE_DIR,
|
|
5
|
+
ResumeState,
|
|
6
|
+
default_state_path,
|
|
7
|
+
state_key,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Executor",
|
|
12
|
+
"ResumeState",
|
|
13
|
+
"default_state_path",
|
|
14
|
+
"state_key",
|
|
15
|
+
"DEFAULT_STATE_DIR",
|
|
16
|
+
]
|
|
@@ -15,6 +15,7 @@ from loguru import logger
|
|
|
15
15
|
from ..detect.remote import RemoteProbe
|
|
16
16
|
from ..models import MigrationPlan, MigrationStep, StepAction, StepStatus
|
|
17
17
|
from ..plugins import PluginContext, registry as _plugin_registry
|
|
18
|
+
from .state import ResumeState, default_state_path
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class ProgressEmitter:
|
|
@@ -124,7 +125,11 @@ class Executor:
|
|
|
124
125
|
progress_yaml: bool = False,
|
|
125
126
|
progress_stream: IO[str] = None,
|
|
126
127
|
audit_log: bool = True,
|
|
127
|
-
audit_path: Optional["Path"] = None
|
|
128
|
+
audit_path: Optional["Path"] = None,
|
|
129
|
+
resume: bool = False,
|
|
130
|
+
from_step: Optional[str] = None,
|
|
131
|
+
state_path: Optional["Path"] = None,
|
|
132
|
+
spec_path: Optional[str] = None):
|
|
128
133
|
self.plan = plan
|
|
129
134
|
self.dry_run = dry_run
|
|
130
135
|
self.probe = RemoteProbe(plan.host)
|
|
@@ -138,10 +143,43 @@ class Executor:
|
|
|
138
143
|
self._audit_path = audit_path
|
|
139
144
|
self._t0: float = 0.0
|
|
140
145
|
|
|
146
|
+
# ── spec path for command_ref resolution ─────────────────────────────
|
|
147
|
+
if spec_path and not plan.spec_path:
|
|
148
|
+
plan.spec_path = spec_path
|
|
149
|
+
|
|
150
|
+
# ── resume / checkpoint ──────────────────────────────────────────────
|
|
151
|
+
self._resume = resume
|
|
152
|
+
self._from_step = from_step
|
|
153
|
+
spec_id = spec_path or plan.spec_path or plan.target_file or plan.infra_file or plan.app
|
|
154
|
+
if state_path is None and not dry_run:
|
|
155
|
+
state_path = default_state_path(spec_id, plan.host)
|
|
156
|
+
self._state_path: Optional[Path] = (
|
|
157
|
+
Path(state_path) if state_path is not None else None
|
|
158
|
+
)
|
|
159
|
+
self._state: Optional[ResumeState] = None
|
|
160
|
+
if self._state_path is not None and not dry_run:
|
|
161
|
+
self._state = ResumeState.load_or_new(
|
|
162
|
+
self._state_path,
|
|
163
|
+
spec_path=str(spec_id),
|
|
164
|
+
host=plan.host,
|
|
165
|
+
total_steps=len(plan.steps),
|
|
166
|
+
)
|
|
167
|
+
# Keep total_steps in sync if the spec changed between runs.
|
|
168
|
+
self._state.total_steps = len(plan.steps)
|
|
169
|
+
|
|
141
170
|
@property
|
|
142
171
|
def completed_steps(self) -> list[MigrationStep]:
|
|
143
172
|
return list(self._completed)
|
|
144
173
|
|
|
174
|
+
@property
|
|
175
|
+
def state(self) -> Optional[ResumeState]:
|
|
176
|
+
"""Current ResumeState (None when dry_run or disabled)."""
|
|
177
|
+
return self._state
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def state_path(self) -> Optional[Path]:
|
|
181
|
+
return self._state_path
|
|
182
|
+
|
|
145
183
|
def run(self) -> bool:
|
|
146
184
|
"""Execute all steps. Returns True if all passed."""
|
|
147
185
|
self._t0 = time.monotonic()
|
|
@@ -151,19 +189,34 @@ class Executor:
|
|
|
151
189
|
if self._emitter:
|
|
152
190
|
self._emitter.start(self.plan)
|
|
153
191
|
|
|
192
|
+
skip_ids = self._compute_skip_set()
|
|
193
|
+
if skip_ids:
|
|
194
|
+
logger.info(f"resume: skipping {len(skip_ids)} already-completed step(s): "
|
|
195
|
+
f"{', '.join(sorted(skip_ids))}")
|
|
196
|
+
|
|
154
197
|
ok = True
|
|
155
198
|
for i, step in enumerate(self.plan.steps, 1):
|
|
199
|
+
if step.id in skip_ids:
|
|
200
|
+
step.status = StepStatus.SKIPPED
|
|
201
|
+
step.result = "resumed: previously completed"
|
|
202
|
+
if self._emitter:
|
|
203
|
+
self._emitter.step_done(i, step)
|
|
204
|
+
continue
|
|
156
205
|
try:
|
|
157
206
|
if self._emitter:
|
|
158
207
|
self._emitter.step_start(i, step)
|
|
159
208
|
self._execute_step(step)
|
|
160
209
|
self._completed.append(step)
|
|
210
|
+
if self._state is not None:
|
|
211
|
+
self._state.mark_done(step.id)
|
|
161
212
|
if self._emitter:
|
|
162
213
|
self._emitter.step_done(i, step)
|
|
163
214
|
except StepError as e:
|
|
164
215
|
logger.error(f"Step failed: {e}")
|
|
165
216
|
step.status = StepStatus.FAILED
|
|
166
217
|
step.error = str(e)
|
|
218
|
+
if self._state is not None:
|
|
219
|
+
self._state.mark_failed(step.id, str(e))
|
|
167
220
|
if self._emitter:
|
|
168
221
|
self._emitter.step_fail(i, step, str(e))
|
|
169
222
|
self._emitter.failed(len(self._completed), len(self.plan.steps), str(e))
|
|
@@ -177,9 +230,40 @@ class Executor:
|
|
|
177
230
|
logger.info(f"{'[DRY RUN] ' if self.dry_run else ''}All {len(self.plan.steps)} steps completed")
|
|
178
231
|
if self._emitter:
|
|
179
232
|
self._emitter.done(len(self.plan.steps))
|
|
233
|
+
# Plan finished cleanly — drop the checkpoint.
|
|
234
|
+
if self._state is not None:
|
|
235
|
+
self._state.remove()
|
|
236
|
+
else:
|
|
237
|
+
if self._state is not None and self._state_path is not None:
|
|
238
|
+
logger.info(f"resume: checkpoint saved → {self._state_path} "
|
|
239
|
+
f"({self._state.completed_count}/{self._state.total_steps} done)")
|
|
180
240
|
self._write_audit(ok=ok, elapsed_s=elapsed)
|
|
181
241
|
return ok
|
|
182
242
|
|
|
243
|
+
def _compute_skip_set(self) -> set[str]:
|
|
244
|
+
"""Determine which step ids should be skipped this run.
|
|
245
|
+
|
|
246
|
+
Sources (in priority order):
|
|
247
|
+
1. ``--from-step <id>``: skip every step BEFORE that id.
|
|
248
|
+
2. ``--resume`` + persisted state: skip every previously completed id.
|
|
249
|
+
"""
|
|
250
|
+
skip: set[str] = set()
|
|
251
|
+
ids = [s.id for s in self.plan.steps]
|
|
252
|
+
|
|
253
|
+
if self._from_step:
|
|
254
|
+
if self._from_step not in ids:
|
|
255
|
+
logger.warning(
|
|
256
|
+
f"--from-step '{self._from_step}' not found in plan; running full plan"
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
idx = ids.index(self._from_step)
|
|
260
|
+
skip.update(ids[:idx])
|
|
261
|
+
|
|
262
|
+
if self._resume and self._state is not None:
|
|
263
|
+
skip.update(self._state.completed_step_ids)
|
|
264
|
+
|
|
265
|
+
return skip
|
|
266
|
+
|
|
183
267
|
def _write_audit(self, *, ok: bool, elapsed_s: float) -> None:
|
|
184
268
|
if not self._audit_log:
|
|
185
269
|
return
|
|
@@ -219,6 +303,7 @@ class Executor:
|
|
|
219
303
|
StepAction.VERSION_CHECK: self._run_version_check,
|
|
220
304
|
StepAction.WAIT: self._run_wait,
|
|
221
305
|
StepAction.PLUGIN: self._run_plugin,
|
|
306
|
+
StepAction.INLINE_SCRIPT: self._run_inline_script,
|
|
222
307
|
}
|
|
223
308
|
|
|
224
309
|
handler = dispatch.get(step.action)
|
|
@@ -456,16 +541,104 @@ class Executor:
|
|
|
456
541
|
step.status = StepStatus.DONE
|
|
457
542
|
step.result = f"waited {total}s"
|
|
458
543
|
|
|
544
|
+
def _run_inline_script(self, step: MigrationStep) -> None:
|
|
545
|
+
"""Execute multiline bash script via SSH using base64 encoding."""
|
|
546
|
+
import base64
|
|
547
|
+
from pathlib import Path
|
|
548
|
+
|
|
549
|
+
script = step.command
|
|
550
|
+
|
|
551
|
+
# If command_ref is set, extract script from markdown file
|
|
552
|
+
if step.command_ref:
|
|
553
|
+
script = self._resolve_command_ref(step.command_ref, step)
|
|
554
|
+
|
|
555
|
+
if not script:
|
|
556
|
+
raise StepError(step, "inline_script requires command field or command_ref with script content")
|
|
557
|
+
|
|
558
|
+
# Base64 encode the script to safely pass it through SSH
|
|
559
|
+
encoded = base64.b64encode(script.encode()).decode()
|
|
560
|
+
timeout = step.timeout or 300
|
|
561
|
+
|
|
562
|
+
# Create temp file, decode script, run it, then clean up
|
|
563
|
+
cmd = (
|
|
564
|
+
f"tmpfile=$(mktemp) && "
|
|
565
|
+
f"echo '{encoded}' | base64 -d > \"$tmpfile\" && "
|
|
566
|
+
f"chmod +x \"$tmpfile\" && "
|
|
567
|
+
f"\"$tmpfile\"; "
|
|
568
|
+
f"rc=$?; "
|
|
569
|
+
f"rm -f \"$tmpfile\"; "
|
|
570
|
+
f"exit $rc"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
r = self.probe.run(cmd, timeout=timeout)
|
|
574
|
+
step.result = r.out[:500] if r.out else "script executed"
|
|
575
|
+
if not r.ok:
|
|
576
|
+
raise StepError(step, f"script failed with exit={r.exit_code}: {r.stderr[:200]}")
|
|
577
|
+
step.status = StepStatus.DONE
|
|
578
|
+
|
|
579
|
+
def _resolve_command_ref(self, command_ref: str, step: MigrationStep) -> str:
|
|
580
|
+
"""Resolve command_ref to script content from markdown file.
|
|
581
|
+
|
|
582
|
+
command_ref formats:
|
|
583
|
+
- "./file.md#section-id" - script from section in specific file
|
|
584
|
+
- "#section-id" - script from section in current spec file
|
|
585
|
+
- "#kiosk-browser-configuration-script" - markpact:ref block
|
|
586
|
+
"""
|
|
587
|
+
from pathlib import Path
|
|
588
|
+
from ..markpact.parser import extract_script_from_markdown, extract_script_by_ref
|
|
589
|
+
|
|
590
|
+
# Parse command_ref
|
|
591
|
+
if "#" in command_ref:
|
|
592
|
+
file_part, section_id = command_ref.split("#", 1)
|
|
593
|
+
file_path = file_part if file_part else getattr(self.plan, 'spec_path', None)
|
|
594
|
+
else:
|
|
595
|
+
section_id = command_ref
|
|
596
|
+
file_path = getattr(self.plan, 'spec_path', None)
|
|
597
|
+
|
|
598
|
+
if not file_path:
|
|
599
|
+
raise StepError(step, f"Cannot resolve command_ref '{command_ref}': no file path available")
|
|
600
|
+
|
|
601
|
+
file_path = Path(file_path)
|
|
602
|
+
if not file_path.exists():
|
|
603
|
+
raise StepError(step, f"Command ref file not found: {file_path}")
|
|
604
|
+
|
|
605
|
+
# Extract script from markdown - try both methods:
|
|
606
|
+
# 1. First try markpact:ref codeblock (new format)
|
|
607
|
+
# 2. Then try section heading (old format)
|
|
608
|
+
markdown_content = file_path.read_text(encoding="utf-8")
|
|
609
|
+
|
|
610
|
+
# Try markpact:ref format first
|
|
611
|
+
script = extract_script_by_ref(markdown_content, section_id, language="bash")
|
|
612
|
+
|
|
613
|
+
# Fallback to section heading format
|
|
614
|
+
if script is None:
|
|
615
|
+
script = extract_script_from_markdown(markdown_content, section_id, language="bash")
|
|
616
|
+
|
|
617
|
+
if script is None:
|
|
618
|
+
raise StepError(step, f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)")
|
|
619
|
+
|
|
620
|
+
return script
|
|
621
|
+
|
|
459
622
|
# ── rollback ──────────────────────────────────────────────────────────────
|
|
460
623
|
|
|
461
624
|
def _rollback(self) -> None:
|
|
462
625
|
logger.warning("Rolling back completed steps...")
|
|
626
|
+
rolled_back: list[str] = []
|
|
463
627
|
for step in reversed(self._completed):
|
|
464
628
|
if step.rollback_command:
|
|
465
629
|
logger.info(f" ↩ rollback [{step.id}]: {step.rollback_command}")
|
|
466
630
|
r = self.probe.run(step.rollback_command, timeout=120)
|
|
467
631
|
if not r.ok:
|
|
468
632
|
logger.warning(f" rollback failed: {r.stderr[:100]}")
|
|
633
|
+
else:
|
|
634
|
+
rolled_back.append(step.id)
|
|
635
|
+
# Forget rolled-back steps so a subsequent --resume re-executes them.
|
|
636
|
+
if rolled_back and self._state is not None:
|
|
637
|
+
self._state.completed_step_ids = [
|
|
638
|
+
sid for sid in self._state.completed_step_ids
|
|
639
|
+
if sid not in set(rolled_back)
|
|
640
|
+
]
|
|
641
|
+
self._state.save()
|
|
469
642
|
|
|
470
643
|
# ── summary ───────────────────────────────────────────────────────────────
|
|
471
644
|
|