- 🤖 LLM usage: $6.7749 (101 commits)
- 👤 Human dev: ~$4201 (42.0h @ $100/h, 30min dedup)
Generated on 2026-05-20 using openrouter/qwen/qwen3-coder-next
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
redeploy detect → live probe host (what is there now)
redeploy plan → migration-plan.yaml (what to do)
redeploy apply → execute plan (do it)
redeploy run → detect + plan + apply (all at once from spec)
redeploy fix → bump + run + LLM heal (smart self-healing deploy)
redeploy bump → bump version in spec (patch/minor/major)
redeploy prompt → NLP → command via LLM (natural language interface)
redeploy mcp → start MCP server (Claude Desktop / VS Code / remote API)
redeploy scan → find devices on LAN (device registry)
redeploy target → deploy to named device (fleet)
redeploy gh-workflow → analyze/run GitHub Actions workflows on demand
# Recommended — installs CLI globally (no venv conflicts)
pipx install redeploy
# Or inside a venv
pip install redeploy
# With doql integration (generates migration.yaml from app.doql):
pip install doql[deploy]# 1. Create spec file
cat > migration.yaml << 'EOF'
name: "myapp deploy 1.0.19 → 1.0.20"
source:
strategy: docker_full
host: root@YOUR_VPS_IP
app: myapp
version: "1.0.19"
target:
strategy: docker_full
host: root@YOUR_VPS_IP
app: myapp
version: "1.0.20"
domain: myapp.example.com
env_file: envs/prod.env
compose_files:
- docker-compose.prod.yml
verify_url: https://myapp.example.com/api/v1/health
verify_version: "1.0.20"
EOF
# 2. Preview steps (no SSH needed)
redeploy run migration.yaml --plan-only
# 3. Dry run (connects via SSH, makes no changes)
redeploy run migration.yaml --dry-run
# 4. Full deploy (live detect → plan → apply)
redeploy run migration.yaml --detect
# Or without --detect (faster, uses spec source as-is)
redeploy run migration.yaml# Register the RPi in the device registry
redeploy device-add pi@192.168.1.42 \
--tag kiosk --tag rpi4 \
--strategy native_kiosk \
--app kiosk-app \
--name "Workshop kiosk #1"
# Preview deploy plan
redeploy target pi@192.168.1.42 migration.yaml --plan-only
# Dry run
redeploy target pi@192.168.1.42 migration.yaml --dry-run
# Deploy
redeploy target pi@192.168.1.42 migration.yaml --detect# Discover SSH-accessible devices on local network (passive: known_hosts + ARP + mDNS)
redeploy scan
# Active ICMP ping sweep (sends packets)
redeploy scan --ping --subnet 192.168.1.0/24
# Try specific SSH users
redeploy scan --user pi --user ubuntu --timeout 8
# List all known devices
redeploy devices
# Filter by tag or strategy
redeploy devices --tag kiosk
redeploy devices --strategy native_kiosk
redeploy devices --reachable # seen in last 5 minutes
# JSON output for scripting
redeploy devices --json | jq '.[] | select(.tags | index("prod"))'
# Add device manually
redeploy device-add root@10.0.0.5 --tag prod --strategy docker_full --app myapp
# Remove device
redeploy device-rm root@10.0.0.5Registry is stored at ~/.config/redeploy/devices.yaml (chmod 600 — safe for SSH key paths).
redeploy supports a declarative configuration workflow for hardware settings — scan to YAML, edit locally, and apply to device:
# 1. Scan hardware state to YAML
redeploy hardware pi@192.168.188.109 > hardware.yaml
redeploy device-map pi@192.168.188.109 > device-map.yaml
# 2. Edit YAML locally (e.g., change display rotation)
# hardware.yaml:
# drm_outputs:
# - name: card0-DSI-2
# connector: DSI-2
# transform: '270' # ← edit this value
# 3. Apply config to remote device
redeploy hardware pi@192.168.188.109 --apply-config hardware.yaml
redeploy device-map pi@192.168.188.109 --apply-config device-map.yamlWhat --apply-config does:
- Applies display transforms via
wlr-randr(Wayland compositor) - Updates kanshi config (
~/.config/kanshi/config) for persistent display rotation - Sets backlight brightness and power state
- Supports both YAML and JSON config files
Supported commands:
redeploy hardware --apply-config FILEredeploy device-map --apply-config FILEredeploy blueprint show FILE --apply-config FILE
Extract specific values from YAML/JSON output using JMESPath query language (similar to XPath for XML):
# Simple path queries
redeploy hardware pi@192.168.188.109 --query "drm_outputs[0].transform"
redeploy hardware pi@192.168.188.109 --query "kernel"
redeploy device-map pi@192.168.188.109 --query "host"
# Filter queries
redeploy hardware pi@192.168.188.109 --query "backlights[?name==\`11-0045\`].brightness"
redeploy hardware pi@192.168.188.109 --query "drm_outputs[?connector==\`DSI-2\`].transform"
# From saved YAML files
redeploy blueprint show blueprint.yaml --query "hardware.drm_outputs[0].transform"
redeploy device-map --show device-map.yaml --query "tags"
# JSON output
redeploy hardware pi@192.168.188.109 --query "tags" --format jsonJMESPath features:
- Simple paths:
kernel,host,board - Array indexing:
drm_outputs[0].transform - Filtering:
[?name==\11-0045`]` - Projections:
backlights[0].[name,brightness] - Wildcards:
drm_outputs[*].transform
Supported commands:
redeploy hardware --query EXPRredeploy device-map --query EXPRredeploy blueprint show FILE --query EXPR
Execute deploy from a YAML spec file (or redeploy.yaml project manifest if no arg).
| Option | Description |
|---|---|
--plan-only |
Show steps without connecting via SSH |
--dry-run |
Connect, show steps, make no changes |
--detect |
Live-probe host before planning (recommended for prod) |
--env NAME |
Use named environment from redeploy.yaml (e.g. prod, rpi5) |
--plan-out FILE |
Save generated plan to file |
redeploy run --env prod # use prod env from redeploy.yaml
redeploy run --env rpi5 --detect # deploy to rpi5 with live probe
redeploy run --dry-run # uses .env DEPLOY_* vars if no redeploy.yamlAnalyze and trigger GitHub Actions workflows from your repo on demand.
Prerequisites:
- GitHub CLI installed:
gh - Authenticated session:
gh auth login - Workflow must define
workflow_dispatchunderon:to be runnable manually
Common usage:
# List all workflow files and dispatch readiness
redeploy gh-workflow list
# Analyze one workflow (triggers/jobs + hint if not dispatchable)
redeploy gh-workflow analyze version-drift
# Analyze all workflows in a custom repo path
redeploy gh-workflow analyze --repo-root /path/to/repo
# Trigger workflow_dispatch run on demand
redeploy gh-workflow run version-drift --ref main
# Pass workflow inputs (repeat --field)
redeploy gh-workflow run release --field env=prod --field force=true
# Trigger and wait for completion (non-zero exit when workflow fails)
redeploy gh-workflow run version-drift --watch
# Preview gh command without executing
redeploy gh-workflow run version-drift --dry-runNotes:
redeploy workflow ...is for workflows fromredeploy.css.redeploy gh-workflow ...is for GitHub Actions in.github/workflows/.
Use top-level hooks: in your migration spec to run custom actions in specific phases.
Supported phases:
before_applybefore_stepafter_stepon_step_failureon_step_retryafter_applyon_failurealways
Minimal example:
hooks:
- id: refresh_cache
phase: after_apply
action: local_cmd
command: "curl -fsS -X POST http://localhost:8100/api/v3/cache/clear || true"
on_failure: warn
- id: open_browser
phase: after_apply
action: open_url
url: http://localhost:8100/
on_failure: warn
- id: before_sync_env_note
phase: before_step
when: "step.id == 'sync_env'"
action: local_cmd
command: "echo '[hook] about to run sync_env'"
on_failure: continueNotes:
when:currently supports simple conditions likestep.id == 'sync_env'andstep.id != 'sync_env'.- Legacy
post_deploy/pre_deployblocks are still accepted and auto-migrated internally. - New specs should use
hooks:only.
Discover SSH-accessible devices on the local network.
| Source | Network activity | Requires |
|---|---|---|
known_hosts |
none | ~/.ssh/known_hosts |
arp |
none | ip neigh / arp -a |
mdns |
passive listen | avahi-browse |
ping_sweep |
ICMP — active | --ping flag |
All SSH-reachable devices are saved to registry. Existing entries updated (last_seen, mac, hostname). Old entries never deleted.
Deploy a spec to a registered device. Device's host, strategy, app, domain are overlaid onto the spec.
redeploy target pi@192.168.1.42 # uses migration.yaml in cwd
redeploy target pi@192.168.1.42 custom.yaml --dry-run
redeploy target prod-vps --detect --plan-onlyAfter successful deploy, a DeployRecord is saved to the device in registry (timestamp, strategy, version, ok/fail).
redeploy detect --host root@VPS_IP --app myapp -o infra.yaml
redeploy plan --infra infra.yaml --target target.yaml -o plan.yaml
redeploy apply --plan plan.yaml
redeploy migrate --host root@VPS_IP --app myapp --target target.yaml # all in one
redeploy init # scaffold migration.yaml + redeploy.yaml
redeploy status # show project manifest summary| Strategy | Description | Use case |
|---|---|---|
docker_full |
Docker Compose — build + up | VPS production |
podman_quadlet |
Rootless Podman systemd units | Quadlet/rootless VPS |
native_kiosk |
systemd + Chromium Openbox | RPi kiosk (no Docker) |
docker_kiosk |
Podman Quadlet in kiosk mode | RPi kiosk with container |
k3s |
Kubernetes/k3s | K3s cluster |
systemd |
Native systemd service | Bare metal |
Generated automatically when strategy: native_kiosk:
rsync_build → sync build/ to device
run_kiosk_installer → bash build/infra/install-kiosk.sh
install_kiosk_service → scp kiosk.service → /etc/systemd/system/
enable_kiosk_service → systemctl enable --now
wait_kiosk_start → 20s
http_health_check → curl http://localhost:8080
rsync_build → sync build/ to device
install_kiosk_quadlet → cp *.container → ~/.config/containers/systemd/ + daemon-reload
start_kiosk_container → systemctl --user restart app.service
wait_kiosk_start → 20s
http_health_check → curl http://localhost:8080
sync_env → scp .env to remote
install_quadlet_files → cp *.container *.network *.volume → ~/.config/containers/systemd/
podman_daemon_reload → systemctl --user daemon-reload
stop_<app> → systemctl --user stop <app>.service
start_<app> → systemctl --user start <app>.service
wait_startup → 15s
http_health_check → verify_url health endpoint
version_check → verify_version match
For system (root) mode, set stop_services: true in target — switches to systemctl (no --user) and /etc/containers/systemd/.
sync_env → scp env_file → remote_dir/.env
docker_build_pull → docker compose build (on remote)
docker_compose_up → docker compose up -d --build
wait_startup → 30s
http_health_check → verify_url health endpoint
version_check → verify_version match
name: "myapp deploy 1.0.19 → 1.0.20"
description: "Production VPS version bump"
source:
strategy: docker_full # docker_full | podman_quadlet | native_kiosk | docker_kiosk | k3s | systemd
host: root@87.106.87.183 # SSH target (user@ip) or "local"
app: myapp
version: "1.0.19"
domain: myapp.example.com
remote_dir: ~/myapp
target:
strategy: docker_full
host: root@87.106.87.183
app: myapp
version: "1.0.20"
domain: myapp.example.com
remote_dir: ~/myapp
compose_files:
- docker-compose.vps.yml
env_file: envs/vps.env
verify_url: https://myapp.example.com/api/v1/health
verify_version: "1.0.20"
extra_steps: # optional — appended or inserted
- id: flush_k3s_iptables # StepLibrary name — no action needed
insert_before: docker_build_pull # inject before specific step
- id: docker_prune # StepLibrary: prune unused images
- id: notify_slack # custom step (needs action:)
action: ssh_cmd
description: "Send deploy notification"
command: "curl -s -X POST $SLACK_WEBHOOK -d '{\"text\":\"deployed 1.0.20\"}'"
risk: lowReference any step by id alone — no action needed. Fields can be overridden:
extra_steps:
- id: flush_k3s_iptables # use as-is
- id: stop_k3s
- id: http_health_check
url: https://myapp.example.com/health # override url
- id: wait_startup_long # 60s instead of 30s| ID | Action | Description |
|---|---|---|
flush_k3s_iptables |
ssh_cmd |
Flush CNI-HOSTPORT-DNAT + KUBE-* chains (stale k3s rules block Docker-proxy on 80/443) |
delete_k3s_ingresses |
kubectl_delete |
Delete all k3s ingresses |
stop_k3s |
systemctl_stop |
Stop k3s service |
disable_k3s |
systemctl_disable |
Disable k3s on boot |
stop_nginx |
systemctl_stop |
Stop host nginx (port 80 conflict) |
restart_traefik |
ssh_cmd |
Restart Traefik container |
docker_prune |
ssh_cmd |
Prune unused images + build cache |
docker_compose_down |
docker_compose_down |
Stop Docker Compose stack |
wait_startup |
wait |
Wait 30s |
wait_startup_long |
wait |
Wait 60s |
http_health_check |
http_check |
Verify health endpoint (expect: healthy) |
version_check |
version_check |
Verify deployed version |
sync_env |
scp |
Copy .env to remote |
podman_daemon_reload |
systemctl_start |
systemctl --user daemon-reload |
stop_podman |
systemctl_stop |
Stop all Podman containers via systemd |
enable_podman_unit |
systemctl_start |
systemctl daemon-reload && enable --now {service}.service |
systemctl_restart |
systemctl_start |
Restart a systemd service (command= to override) |
systemctl_daemon_reload |
ssh_cmd |
systemctl daemon-reload |
git_pull |
ssh_cmd |
git pull --ff-only with rollback (git reset --hard HEAD@{1}) |
By default extra steps are appended after all generated steps. Use insert_before: <step_id> to inject at a specific position:
extra_steps:
- id: flush_k3s_iptables
insert_before: docker_build_pull # runs before build, not after verifyBeyond http_check/inline_script, redeploy can run query-language test
scenarios as post-deploy verification. Each runs the interpreter locally
(on the controller, where it is installed) and targets the freshly deployed
host via url. The step fails — and thus the deploy fails/rolls back — when the
scenario fails.
| Action | Default runner | Verdict | Purpose |
|---|---|---|---|
testql |
testql run --url <url> <file> |
exit 0 (+ optional expect in output) |
API/GUI smoke of the deployed app |
oql |
oqlctl <file> -m <mode> --json [--firmware-url <url>] |
JSON ok == true (fallback: exit 0) |
run an OQL scenario against the deployed runtime/firmware |
aql |
aql <file> [locale] |
exit 0 (+ optional expect, e.g. a variant name) |
evaluate an AQL decision model and assert the verdict |
Fields (on top of url / expect):
query_source— path to the.testql.toon.yaml/.oql/.aqlscenario.query_mode— OQL mode:validate|dry-run|execute.query_runner— override the runner binary (absolute path is fine).query_locale— AQL locale (en|pl).query_context— AQL: path to a context JSON for resolve.command— full command-template override; placeholders{url} {source} {mode} {locale} {context}are substituted (literal{}in the command are left intact — nostr.format).
extra_steps:
- id: post_deploy_oql
action: oql
description: "OQL: real sensor read from the deployed firmware"
query_source: deploy-tests/oql/firmware-sensor-read.oql
query_mode: execute
url: http://192.168.188.122:8202 # oqlctl --firmware-url
- id: post_deploy_testql
action: testql
description: "TestQL: API smoke of the deployed app"
query_source: deploy-tests/testql/post-deploy-api-smoke.testql.toon.yaml
flags: ["--output", "json", "--quiet"]
url: http://192.168.188.109:8100 # testql run --url
- id: post_deploy_aql
action: aql
description: "AQL: decision model returns the expected variant"
query_source: deploy-tests/aql/personalization.en.aql
command: "python3 /path/to/aql/cli.py {source} {locale}"
query_locale: en
expect: technical_deepA ready example spec lives in c2004: deploy-tests/post-deploy.redeploy.md
(run with redeploy run deploy-tests/post-deploy.redeploy.md).
Extend the step pipeline with custom action types using action: plugin:
extra_steps:
- id: reload_kiosk
action: plugin
plugin_type: browser_reload
description: Reload kiosk browser after deploy
plugin_params:
port: 9222
ignore_cache: true
url_contains: "localhost:8100"plugin_type |
Description | plugin_params |
|---|---|---|
browser_reload |
Reload Chromium via CDP (Chrome DevTools Protocol) over SSH | port (9222), ignore_cache (true), url_contains ("") |
Place a .py file in ./redeploy_plugins/ (project-local) or ~/.redeploy/plugins/ (user-global):
# ./redeploy_plugins/notify.py
from redeploy.plugins import register_plugin, PluginContext
from redeploy.models import StepStatus
@register_plugin("notify_slack")
def notify_slack(ctx: PluginContext) -> None:
webhook = ctx.params["webhook"]
ctx.probe.run(f"curl -X POST {webhook} -d '{{\"text\":\"deployed!\"}}'")
ctx.step.result = "notified"
ctx.step.status = StepStatus.DONEPluginContext fields:
| Field | Type | Description |
|---|---|---|
step |
MigrationStep |
Current step — set result and status here |
host |
str |
SSH host (e.g. pi@192.168.1.5) |
probe |
RemoteProbe |
Call probe.run(cmd) for remote SSH commands |
emitter |
ProgressEmitter? |
Emit mid-step progress: emitter.progress(step.id, msg) |
params |
dict |
Shortcut for step.plugin_params |
dry_run |
bool |
Skip side-effects if True |
Execute multiline bash scripts directly from YAML without external files:
extra_steps:
- id: configure_kiosk
action: inline_script
description: "Deploy kiosk launch script"
command: |
#!/bin/bash
mkdir -p ~/c2004/config
cat > ~/c2004/config/kiosk-launch.sh << 'EOF'
#!/bin/bash
if command -v chromium-browser >/dev/null 2>&1; then
chromium-browser --kiosk http://localhost:8100
elif command -v firefox >/dev/null 2>&1; then
firefox --kiosk http://localhost:8100
fi
EOF
chmod +x ~/c2004/config/kiosk-launch.sh
risk: medium
timeout: 60The script is base64-encoded and executed via SSH with automatic temp file cleanup. Use command field for multiline script content (YAML | preserves newlines).
Instead of duplicating scripts in YAML, reference a script defined in a markdown codeblock:
extra_steps:
- id: configure_kiosk
action: inline_script
description: "Execute kiosk script from markdown"
command_ref: "#kiosk-browser-configuration-script"
risk: mediumIn your migration markdown file, define the script in a section:
## Kiosk Browser Configuration Script
```bash
#!/bin/bash
# Auto-detect browser...
if command -v chromium-browser >/dev/null 2>&1; then
chromium-browser --kiosk http://localhost:8100
fi
**Benefits:**
- Single source of truth — script lives in one place (markdown codeblock)
- No duplication between markdown documentation and YAML
- Easy to read and maintain
- Changes to the codeblock automatically apply to the deployment
**Reference formats:**
- `"#section-id"` — script from section in current spec file
- `"./file.md#section-id"` — script from section in specific file
The section ID is derived from the heading: spaces become hyphens, lowercase.
Example: `## Kiosk Browser Configuration Script` → `#kiosk-browser-configuration-script`
### Execute Script by Reference (`redeploy exec`)
Run a single script from markdown without running the full migration:
```bash
# Execute script from codeblock on remote host
redeploy exec '#kiosk-browser-configuration-script' \
--host pi@192.168.188.108 \
--file migration.podman-rpi5-resume.md
# With file in reference
redeploy exec './migration.md#install-deps' --host root@server.com
# Using markpact:ref (more explicit)
redeploy exec 'kiosk-script-id' --host pi@192.168.188.108 --file migration.md
# Dry-run to preview script
redeploy exec '#backup-script' --host pi@192.168.188.108 --file ops.md --dry-run
This is useful for:
- One-off operations defined in markdown docs
- Testing individual scripts before full migration
- Running maintenance tasks
Test multiple scripts at once:
# Execute multiple scripts by ref
redeploy exec-multi 'kiosk-script,install-deps,cleanup' \
--host pi@192.168.188.108 \
--file migration.md
# Mix of markpact:ref and section headings
redeploy exec-multi 'script1,#section2,script3' \
--host root@server.com \
--file deploy.md \
--dry-runFor more explicit script identification, use markpact:ref <id> in codeblock:
```bash markpact:ref kiosk-browser-configuration-script
#!/bin/bash
# Auto-detect browser...
if command -v chromium-browser >/dev/null 2>&1; then
chromium-browser --kiosk http://localhost:8100
fi
Benefits of `markpact:ref`:
- Explicit ID assignment (not derived from heading)
- Multiple scripts per section
- Can reference by simple ID instead of full heading
- Self-documenting in markdown
Place in project root — `redeploy run` (no args) uses it automatically.
Supports **named environments** for multi-target projects:
```yaml
spec: migration.yaml # default spec file
app: myapp
environments:
prod:
host: root@87.106.87.183
strategy: docker_full
domain: myapp.example.com
env_file: envs/vps.env
verify_url: https://myapp.example.com/api/v1/health
rpi5:
host: pi@192.168.188.108
strategy: systemd
env_file: .env
verify_url: http://192.168.188.108:8000/api/v1/health
dev:
host: local
strategy: docker_full
env_file: .env.local
verify_url: http://localhost:8000/api/v1/health
Fallback: if no redeploy.yaml found, redeploy run reads DEPLOY_* vars from .env:
# .env
DEPLOY_HOST=pi@192.168.1.5
DEPLOY_APP=myapp
DEPLOY_DOMAIN=myapp.local
DEPLOY_ENV_FILE=.envredeploy is the deploy engine for doql declarative apps.
# Install with doql integration
pip install doql[deploy]
# doql build generates build/infra/migration.yaml automatically
DEPLOY_HOST=root@YOUR_VPS doql build
# Then deploy — no args needed
doql deploy # calls redeploy API internally
doql deploy --plan-only
doql deploy --dry-run
doql quadlet --install # installs Quadlet units via redeploydoql DEPLOY.target → redeploy strategy mapping:
| doql | redeploy |
|---|---|
docker-compose |
docker_full |
quadlet |
podman_quadlet |
kiosk-appliance |
native_kiosk |
kubernetes |
k3s |
IaC/CI config coverage (via redeploy import, used by doql/redeploy workflows):
- Docker Compose + Dockerfile
- nginx configs (
nginx.conf,*.conf) - Kubernetes manifests (
apiVersion+kindYAML) - Terraform (
*.tf,*.tfvars) - TOML (
pyproject.toml, app/tool TOML) - Vite config (
vite.config.ts/js/mjs/cjs) - CI/CD: GitHub Actions, GitLab CI, Jenkinsfile
Parser plugin extension:
- Python entry points:
redeploy.iac.parsers - Project-local parsers:
./redeploy_iac_parsers/*.py - User-global parsers:
~/.redeploy/iac_parsers/*.py
Built-in template generator:
redeploy import --list-plugin-templatesredeploy import --plugin-template helm-kustomizeredeploy import --plugin-template argocd-flux --plugin-dir redeploy_iac_parsers
Example external plugin (Helm + Ansible):
- Source template:
examples/redeploy_iac_parsers/helm_ansible.py - Quick start:
mkdir -p redeploy_iac_parserscp examples/redeploy_iac_parsers/helm_ansible.py redeploy_iac_parsers/redeploy import path/to/Chart.yamlredeploy import path/to/playbook.yml
Example external plugin (Helm templates + Kustomize):
- Source template:
examples/redeploy_iac_parsers/helm_kustomize.py - Quick start:
mkdir -p redeploy_iac_parserscp examples/redeploy_iac_parsers/helm_kustomize.py redeploy_iac_parsers/redeploy import path/to/chart/templates/deployment.yamlredeploy import path/to/kustomization.yaml
Example external plugin (ArgoCD Application + Flux Kustomization):
- Source template:
examples/redeploy_iac_parsers/argocd_flux.py - Quick start:
mkdir -p redeploy_iac_parserscp examples/redeploy_iac_parsers/argocd_flux.py redeploy_iac_parsers/redeploy import path/to/argocd-application.yamlredeploy import path/to/flux-kustomization.yaml
Example external plugin (GitOps CI for ArgoCD/Flux):
- Source template:
examples/redeploy_iac_parsers/gitops_ci.py - Quick start:
mkdir -p redeploy_iac_parserscp examples/redeploy_iac_parsers/gitops_ci.py redeploy_iac_parsers/redeploy import .github/workflows/deploy-gitops.ymlredeploy import .gitlab-ci.yml
| Directory | Scenario | Strategy |
|---|---|---|
01-vps-version-bump |
VPS Docker version bump | docker_full → docker_full |
02-k3s-to-docker |
Migrate off k3s | k3s → docker_full |
03-docker-to-podman-quadlet |
Move to rootless Podman | docker_full → podman_quadlet |
04-rpi-kiosk |
Raspberry Pi kiosk update | native_kiosk → native_kiosk |
05-iot-fleet-ota |
IoT fleet OTA update | docker_full → docker_full |
09-fleet-yaml |
Fleet with stages + scan | fleet + redeploy target |
11-traefik-tls |
Traefik + Let's Encrypt | docker_full → podman_quadlet |
12-ci-pipeline |
GitHub Actions / GitLab CI | CI-triggered docker_full |
# Run any example in dry-run mode (no SSH required):
redeploy run examples/01-vps-version-bump/migration.yaml --plan-only
redeploy run examples/04-rpi-kiosk/migration.yaml --plan-onlyredeploy fix is the recommended day-to-day deploy command. It:
- bumps the patch version in
VERSION+ spec header - applies the migration spec with
--healenabled - if a step fails, calls an LLM (via LiteLLM / OpenRouter) to suggest a fix and retries automatically
# Self-healing deploy: bump version → run → LLM retry on failure
redeploy fix .
redeploy fix redeploy/pi109/migration.md
# With a problem hint for the LLM
redeploy fix . --hint "service not starting after update"
redeploy fix . --hint "brak ikon SVG w menu"
# Preview only (no apply)
redeploy fix . --dry-run
# Bump minor version instead of patch
redeploy fix . --minor
# Bump major version
redeploy fix . --major
redeploy fix . --retries 5
# Skip version bump
redeploy fix . --no-bumpSpec discovery from .:
./migration.mdor./migration.yaml— direct match./redeploy/<target>/migration.md— project pattern (lists targets, asks if multiple)- Recursive fallback anywhere under the directory
redeploy fix automatically discovers migration specs — running from project root
with multiple targets prompts interactively.
# Bump patch (default): 1.0.31 → 1.0.32
redeploy bump .
redeploy bump redeploy/pi109/migration.md
# Bump minor: 1.0.31 → 1.1.0
redeploy bump . --minor
# Bump major: 1.0.31 → 2.0.0
redeploy bump . --majorUpdates VERSION file and all version references in the migration spec
(version:, name: "... vX.Y.Z", description: "... vX.Y.Z").
redeploy run also supports --heal mode (enabled by default):
# Run with LLM self-healing (default)
redeploy run migration.yaml
# Disable healing
redeploy run migration.yaml --no-heal
# Pass problem description to LLM
redeploy run migration.yaml --fix "nginx port conflict"
# Max heal retries
redeploy run migration.yaml --max-heal-retries 5LLM reads the failed step output, runs SSH diagnostics, and patches the spec YAML.
Repairs are logged to REPAIR_LOG.md next to the spec.
Requires OPENROUTER_API_KEY (or OPENAI_API_KEY) in .env or ~/.redeploy/.env.
Model defaults to openrouter/qwen/qwen3-coder-next (override with LLM_MODEL=...).
# Map a natural language instruction to a redeploy command
redeploy prompt "deploy c2004 to pi109"
redeploy prompt "pokaż plan deployu na pi109"
redeploy prompt "bump version and redeploy" --yes
redeploy prompt "what specs are available?" --schema-only
# Force dry-run on generated command
redeploy prompt "run the pi109 migration" --dry-run
# Skip confirmation
redeploy prompt "fix the frontend service" --yes
# Preview the workspace schema sent to the LLM
redeploy prompt "..." --show-schemaThe LLM receives a workspace schema (discovered specs, version, git branch, command catalogue)
and maps the instruction to a concrete redeploy invocation.
Language is auto-detected — Polish, English, or any language the model supports.
redeploy exposes an MCP (Model Context Protocol) server, letting AI assistants (Claude Desktop, VS Code Copilot, custom agents) call redeploy operations as structured tools.
# stdio — for Claude Desktop / VS Code local integration
redeploy mcp
# HTTP SSE — for remote/shared access
redeploy mcp --transport sse --port 8811
# Streamable HTTP
redeploy mcp --transport http --port 8811
# Standalone binary (no CLI wrapper)
redeploy-mcp --transport sse| Tool | Description |
|---|---|
schema |
Discover workspace: specs, version, git branch, command catalogue |
list_specs |
List all migration specs found in a directory |
plan_spec |
Preview a spec (dry-run) — safe, no changes |
run_spec |
Apply a migration spec |
fix_spec |
Self-healing deploy: bump → apply → LLM retry |
bump_version |
Bump patch/minor/major version |
diagnose |
SSH diagnostics on a remote host |
exec_ssh |
Run an ad-hoc command on a remote host |
nlp_command |
Translate NLP instruction → redeploy command |
| URI | Description |
|---|---|
redeploy://workspace |
Current workspace schema as JSON |
redeploy://spec/{path} |
Raw content of a migration spec file |
Add to ~/.config/claude/claude_desktop_config.json:
{
"mcpServers": {
"redeploy": {
"command": "redeploy",
"args": ["mcp"]
}
}
}Add to .vscode/mcp.json in your workspace:
{
"servers": {
"redeploy": {
"type": "stdio",
"command": "redeploy",
"args": ["mcp"]
}
}
}Or for an SSE server already running on a remote machine:
{
"servers": {
"redeploy-remote": {
"type": "sse",
"url": "http://192.168.188.109:8811/sse"
}
}
}pip install "redeploy[mcp]"Probe and diagnose hardware on a remote host (DSI display, DRM connectors, backlight, I2C, config.txt, Wayland compositor).
| Option | Description |
|---|---|
| `--format [yaml | json]` |
--fix |
Print fix commands for all issues found |
--apply-fix COMPONENT |
Run fix for specific component via SSH |
--panel PANEL_ID |
Specify panel ID explicitly |
--list-panels |
List available panel definitions |
--set-transform TRANSFORM |
Set display rotation for DSI output (normal, 90, 180, 270, flipped, etc.) |
--apply-config FILE |
Apply display settings from YAML/JSON config file |
--query EXPR |
Extract specific values using JMESPath query |
--ssh-key PATH |
SSH private key path |
redeploy hardware pi@192.168.188.109
redeploy hardware pi@192.168.188.109 --fix
redeploy hardware pi@192.168.188.109 --set-transform 270
redeploy hardware pi@192.168.188.109 --apply-config hardware.yaml
redeploy hardware pi@192.168.188.109 --query "drm_outputs[0].transform"Generate full device snapshot (hardware + infra + diagnostics).
| Option | Description |
|---|---|
--name TEXT |
Human-friendly device label |
--tag TEXT |
Tag(s) to attach (repeatable) |
--save |
Persist map to ~/.config/redeploy/device-maps/ |
--out PATH |
Save to specific file |
| `--format [yaml | json]` |
--no-infra |
Skip infra probe (hardware only) |
--list |
List saved device maps |
--show PATH |
Load and display saved device-map file |
--diff PATH... |
Diff two saved device-map files |
--apply-config FILE |
Apply hardware/infra settings from YAML config file |
--query EXPR |
Extract specific values using JMESPath query |
--ssh-key PATH |
SSH private key path |
redeploy device-map pi@192.168.188.109 --save --name "kiosk-lab"
redeploy device-map --list
redeploy device-map --show ~/.config/redeploy/device-maps/pi_at_192.168.188.109.yaml
redeploy device-map pi@192.168.188.109 --apply-config device-map.yaml
redeploy device-map pi@192.168.188.109 --query "hardware.drm_outputs[0].transform"Manage device blueprints (capture, show, list, twin, migrate).
Capture device state as blueprint.
| Option | Description |
|---|---|
| `--format [yaml | json]` |
--save |
Persist to ~/.config/redeploy/blueprints/ |
--out PATH |
Save to specific file |
--ssh-key PATH |
SSH private key path |
Display saved blueprint.
| Option | Description |
|---|---|
| `--format [yaml | json]` |
--apply-config FILE |
Apply blueprint settings from YAML config file |
--query EXPR |
Extract specific values using JMESPath query |
redeploy blueprint capture pi@192.168.188.109 > blueprint.yaml
redeploy blueprint show blueprint.yaml --apply-config blueprint.yaml
redeploy blueprint show blueprint.yaml --query "hardware.drm_outputs[0].transform"Core runtime dependencies:
| Package | Purpose |
|---|---|
pydantic>=2.0 |
Data validation and settings |
pyyaml>=6.0 |
YAML parsing/serialization |
markdown-it-py>=3.0 |
Markdown parsing (markpact specs) |
click>=8.0 |
CLI framework |
loguru>=0.7 |
Structured logging |
paramiko>=3.0 |
SSH client |
httpx>=0.25 |
HTTP client |
rich>=13.0 |
Terminal UI |
jmespath>=1.0 |
JSON/YAML query expressions |
goal>=2.1.0 |
Goal tracking |
costs>=0.1.20 |
AI cost tracking |
pfix>=0.1.60 |
Self-healing Python |
Optional dependencies:
| Package | Purpose |
|---|---|
op3>=0.1.8 |
OP3 support |
mcp>=1.0 |
MCP server mode |
markpact— Markdown-native deployment spec format (markpact:config, markpact:steps)goal— Migration goal tracking and validationcosts— AI cost tracking and reporting
Licensed under Apache-2.0.