redeploy 0.2.17__tar.gz → 0.2.19__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. {redeploy-0.2.17 → redeploy-0.2.19}/PKG-INFO +108 -6
  2. {redeploy-0.2.17 → redeploy-0.2.19}/README.md +107 -5
  3. {redeploy-0.2.17 → redeploy-0.2.19}/pyproject.toml +1 -1
  4. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/apply/executor.py +67 -5
  6. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/cli.py +765 -290
  7. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/markpact/__init__.py +11 -1
  8. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/markpact/compiler.py +7 -2
  9. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/markpact/models.py +3 -1
  10. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/markpact/parser.py +111 -4
  11. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/models.py +2 -0
  12. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_executor.py +56 -0
  13. redeploy-0.2.19/redeploy/tests/test_markpact_parser.py +93 -0
  14. redeploy-0.2.19/redeploy/tests/test_resume.py +271 -0
  15. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy.egg-info/PKG-INFO +108 -6
  16. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy.egg-info/SOURCES.txt +1 -1
  17. redeploy-0.2.17/redeploy/checkpoint.py +0 -234
  18. redeploy-0.2.17/redeploy/tests/test_markpact_parser.py +0 -41
  19. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/apply/__init__.py +0 -0
  20. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/apply/state.py +0 -0
  21. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/audit.py +0 -0
  22. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/data_sync.py +0 -0
  23. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/detect/__init__.py +0 -0
  24. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/detect/detector.py +0 -0
  25. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/detect/probes.py +0 -0
  26. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/detect/remote.py +0 -0
  27. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/detect/templates.py +0 -0
  28. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/detect/workflow.py +0 -0
  29. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/discovery.py +0 -0
  30. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl/__init__.py +0 -0
  31. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl/loader.py +0 -0
  32. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl/parser.py +0 -0
  33. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl_python/__init__.py +0 -0
  34. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl_python/context.py +0 -0
  35. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl_python/decorators.py +0 -0
  36. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl_python/docker_steps.py +0 -0
  37. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl_python/exceptions.py +0 -0
  38. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl_python/runner.py +0 -0
  39. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/dsl_python/steps.py +0 -0
  40. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/fleet.py +0 -0
  41. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/iac/__init__.py +0 -0
  42. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/iac/base.py +0 -0
  43. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/iac/docker_compose.py +0 -0
  44. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/iac/parsers/__init__.py +0 -0
  45. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/iac/parsers/compose.py +0 -0
  46. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/iac/registry.py +0 -0
  47. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/observe.py +0 -0
  48. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/parse.py +0 -0
  49. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/patterns.py +0 -0
  50. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/plan/__init__.py +0 -0
  51. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/plan/planner.py +0 -0
  52. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/plugins/__init__.py +0 -0
  53. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/plugins/builtin/__init__.py +0 -0
  54. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/plugins/builtin/browser_reload.py +0 -0
  55. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/plugins/builtin/notify.py +0 -0
  56. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  57. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/spec_loader.py +0 -0
  58. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/ssh.py +0 -0
  59. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/steps.py +0 -0
  60. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/__init__.py +0 -0
  61. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_audit.py +0 -0
  62. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_cli.py +0 -0
  63. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_data_sync.py +0 -0
  64. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_discovery.py +0 -0
  65. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_dsl.py +0 -0
  66. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_examples.py +0 -0
  67. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_fleet.py +0 -0
  68. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_git_integration.py +0 -0
  69. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_iac.py +0 -0
  70. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_manifest.py +0 -0
  71. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_markpact_compiler.py +0 -0
  72. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_models.py +0 -0
  73. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_observe.py +0 -0
  74. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_parse.py +0 -0
  75. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_patterns.py +0 -0
  76. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_planner_edge.py +0 -0
  77. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_plugins.py +0 -0
  78. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_probes.py +0 -0
  79. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_public_api.py +0 -0
  80. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_spec_loader.py +0 -0
  81. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_ssh.py +0 -0
  82. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_templates.py +0 -0
  83. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_verify.py +0 -0
  84. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_version.py +0 -0
  85. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_version_changelog.py +0 -0
  86. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_version_cli.py +0 -0
  87. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_version_manifest.py +0 -0
  88. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_version_sources.py +0 -0
  89. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/tests/test_workflow.py +0 -0
  90. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/verify.py +0 -0
  91. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/__init__.py +0 -0
  92. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/bump.py +0 -0
  93. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/changelog.py +0 -0
  94. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/commits.py +0 -0
  95. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/diff.py +0 -0
  96. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/git_integration.py +0 -0
  97. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/git_transaction.py +0 -0
  98. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/manifest.py +0 -0
  99. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/sources/__init__.py +0 -0
  100. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/sources/base.py +0 -0
  101. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/sources/json_.py +0 -0
  102. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/sources/plain.py +0 -0
  103. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/sources/regex.py +0 -0
  104. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/sources/toml_.py +0 -0
  105. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/sources/yaml_.py +0 -0
  106. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version/transaction.py +0 -0
  107. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy/version.py +0 -0
  108. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy.egg-info/dependency_links.txt +0 -0
  109. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy.egg-info/entry_points.txt +0 -0
  110. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy.egg-info/requires.txt +0 -0
  111. {redeploy-0.2.17 → redeploy-0.2.19}/redeploy.egg-info/top_level.txt +0 -0
  112. {redeploy-0.2.17 → redeploy-0.2.19}/setup.cfg +0 -0
  113. {redeploy-0.2.17 → redeploy-0.2.19}/tests/test_iac.py +0 -0
  114. {redeploy-0.2.17 → redeploy-0.2.19}/tests/test_parse.py +0 -0
  115. {redeploy-0.2.17 → redeploy-0.2.19}/tests/test_redeploy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.17
3
+ Version: 0.2.19
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.17-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.40-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-11.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
30
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.19-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
31
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.70-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-13.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
32
 
33
- - 🤖 **LLM usage:** $5.4000 (36 commits)
34
- - 👤 **Human dev:** ~$1130 (11.3h @ $100/h, 30min dedup)
33
+ - 🤖 **LLM usage:** $5.7000 (38 commits)
34
+ - 👤 **Human dev:** ~$1297 (13.0h @ $100/h, 30min dedup)
35
35
 
36
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
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.17-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.19-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
41
 
42
42
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
43
43
 
@@ -428,6 +428,108 @@ extra_steps:
428
428
 
429
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
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
+
431
533
 
432
534
 
433
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.17-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.40-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-11.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.19-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$5.70-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-13.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $5.4000 (36 commits)
10
- - 👤 **Human dev:** ~$1130 (11.3h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $5.7000 (38 commits)
10
+ - 👤 **Human dev:** ~$1297 (13.0h @ $100/h, 30min dedup)
11
11
 
12
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
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.17-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.19-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
19
19
 
@@ -404,6 +404,108 @@ extra_steps:
404
404
 
405
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
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
+
407
509
 
408
510
 
409
511
  Place in project root — `redeploy run` (no args) uses it automatically.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.17"
7
+ version = "0.2.19"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
7
7
 
8
8
  Everything not listed in ``__all__`` is internal and may change without notice.
9
9
  """
10
- __version__ = "0.2.17"
10
+ __version__ = "0.2.19"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -143,10 +143,14 @@ class Executor:
143
143
  self._audit_path = audit_path
144
144
  self._t0: float = 0.0
145
145
 
146
+ # ── spec path for command_ref resolution ─────────────────────────────
147
+ if spec_path and not plan.spec_path:
148
+ plan.spec_path = spec_path
149
+
146
150
  # ── resume / checkpoint ──────────────────────────────────────────────
147
151
  self._resume = resume
148
152
  self._from_step = from_step
149
- spec_id = spec_path or plan.target_file or plan.infra_file or plan.app
153
+ spec_id = spec_path or plan.spec_path or plan.target_file or plan.infra_file or plan.app
150
154
  if state_path is None and not dry_run:
151
155
  state_path = default_state_path(spec_id, plan.host)
152
156
  self._state_path: Optional[Path] = (
@@ -292,6 +296,7 @@ class Executor:
292
296
  StepAction.DOCKER_BUILD: self._run_docker_build,
293
297
  StepAction.DOCKER_HEALTH_WAIT: self._run_docker_health_wait,
294
298
  StepAction.CONTAINER_LOG_TAIL: self._run_container_log_tail,
299
+ StepAction.PODMAN_BUILD: self._run_podman_build,
295
300
  StepAction.SSH_CMD: self._run_ssh,
296
301
  StepAction.SCP: self._run_scp,
297
302
  StepAction.RSYNC: self._run_rsync,
@@ -399,6 +404,54 @@ class Executor:
399
404
  logger.debug(f" build completed in {elapsed + poll_interval}s")
400
405
  step.status = StepStatus.DONE
401
406
 
407
+ def _run_podman_build(self, step: MigrationStep) -> None:
408
+ """Run podman build on remote with periodic progress polling."""
409
+ cmd = step.command
410
+ if not cmd:
411
+ raise StepError(step, "No command specified")
412
+
413
+ timeout = step.timeout or 1800 # 30 min default for ARM64 builds
414
+ poll_interval = 15 # seconds between progress snapshots
415
+ done_event = threading.Event()
416
+ result_holder: list = []
417
+
418
+ def _ssh_build() -> None:
419
+ r = self.probe.run(cmd, timeout=timeout)
420
+ result_holder.append(r)
421
+ done_event.set()
422
+
423
+ thread = threading.Thread(target=_ssh_build, daemon=True)
424
+ thread.start()
425
+
426
+ elapsed = 0
427
+ while not done_event.wait(timeout=poll_interval):
428
+ elapsed += poll_interval
429
+ snap = self.probe.run(
430
+ "podman system df 2>/dev/null | grep -E 'Image|Cache' | "
431
+ "awk '{print $1, $3, $4}'",
432
+ timeout=10,
433
+ )
434
+ if snap.ok and snap.out.strip():
435
+ lines = " | ".join(snap.out.strip().splitlines())
436
+ msg = f"[{elapsed}s] podman cache: {lines}"
437
+ logger.debug(f" {msg}")
438
+ if self._emitter:
439
+ self._emitter.progress(step.id, msg)
440
+ else:
441
+ msg = f"[{elapsed}s] podman build in progress..."
442
+ logger.debug(f" {msg}")
443
+ if self._emitter:
444
+ self._emitter.progress(step.id, msg)
445
+
446
+ if not result_holder:
447
+ raise StepError(step, "Podman build thread did not return a result")
448
+ r = result_holder[0]
449
+ step.result = r.out[:500]
450
+ if not r.ok:
451
+ raise StepError(step, f"exit={r.exit_code}: {r.stderr[:300]}")
452
+ logger.debug(f" podman build completed in {elapsed + poll_interval}s")
453
+ step.status = StepStatus.DONE
454
+
402
455
  def _run_docker_health_wait(self, step: MigrationStep) -> None:
403
456
  """Wait until all containers reach 'healthy' or 'running' status.
404
457
 
@@ -578,9 +631,10 @@ class Executor:
578
631
  command_ref formats:
579
632
  - "./file.md#section-id" - script from section in specific file
580
633
  - "#section-id" - script from section in current spec file
634
+ - "#kiosk-browser-configuration-script" - markpact:ref block
581
635
  """
582
636
  from pathlib import Path
583
- from ..markpact.parser import extract_script_from_markdown
637
+ from ..markpact.parser import extract_script_from_markdown, extract_script_by_ref
584
638
 
585
639
  # Parse command_ref
586
640
  if "#" in command_ref:
@@ -597,12 +651,20 @@ class Executor:
597
651
  if not file_path.exists():
598
652
  raise StepError(step, f"Command ref file not found: {file_path}")
599
653
 
600
- # Extract script from markdown
654
+ # Extract script from markdown - try both methods:
655
+ # 1. First try markpact:ref codeblock (new format)
656
+ # 2. Then try section heading (old format)
601
657
  markdown_content = file_path.read_text(encoding="utf-8")
602
- script = extract_script_from_markdown(markdown_content, section_id, language="bash")
658
+
659
+ # Try markpact:ref format first
660
+ script = extract_script_by_ref(markdown_content, section_id, language="bash")
661
+
662
+ # Fallback to section heading format
663
+ if script is None:
664
+ script = extract_script_from_markdown(markdown_content, section_id, language="bash")
603
665
 
604
666
  if script is None:
605
- raise StepError(step, f"Could not find bash script in section '{section_id}' of {file_path}")
667
+ raise StepError(step, f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)")
606
668
 
607
669
  return script
608
670