diff --git a/CHANGELOG.md b/CHANGELOG.md index b61480f..eb252f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to `devloop` will be recorded in this file. ## [Unreleased] +## [0.9.3] - 2026-07-22 + +### Changed +- Clarified how to watch sibling directories by setting `root` to their + common parent, and documented the `restart = "never"` default plus the + wrapper exit-status behavior of `on_failure`. + +### Fixed +- Made the cross-platform runtime smoke test use output-derived state + readiness instead of a fixture-local HTTP server, removing its flaky + loopback startup dependency while preserving workflow and watch coverage. + ## [0.9.2] - 2026-07-01 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 73e01f1..60a19ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,7 +235,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "devloop" -version = "0.9.2" +version = "0.9.3" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index b555784..0e30176 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "devloop" -version = "0.9.2" +version = "0.9.3" edition = "2024" [dependencies] diff --git a/docs/behavior.md b/docs/behavior.md index 4466833..c995f39 100644 --- a/docs/behavior.md +++ b/docs/behavior.md @@ -108,6 +108,12 @@ Managed processes are long-running child commands. `devloop` is shutting down. - `restart = "on_failure"` restarts only after unsuccessful exit. - `restart = "never"` never restarts automatically. +- Restart policies use the managed command's exit status. A wrapper that + exits successfully after its long-running child dies is not restarted by + `on_failure`; the service remains down. For development-server wrappers, + use `restart = "always"` or propagate the child's exit status. A liveness + probe can detect a dead child only while its wrapper remains running; a + readiness probe only checks startup and does not supervise ongoing health. - Managed child processes inherit the ambient environment unless the process config explicitly overrides individual variables such as `env.RUST_LOG`. diff --git a/docs/configuration.md b/docs/configuration.md index 513c457..d7909cc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -76,6 +76,24 @@ always watching the whole repository root recursively. `native` remains the default backend; `poll` exists as a fallback for environments where filesystem notifications are unreliable. +### Watching a sibling directory + +Watch patterns cannot match paths outside `root`. If the configuration is +in `devloop/` and you need to watch its sibling `skills/` directory, make +their common parent the root and use a root-relative path: + +```toml +root = ".." + +[watch.skills] +paths = ["skills/"] +workflow = "skills" +``` + +`root` also resolves relative process commands, `cwd` values, and the +default state-file location. Adjust those paths if they were previously +relative to `devloop/`. + ## Processes Processes are long-running commands supervised by `devloop`. @@ -96,7 +114,8 @@ output = { inherit = true, body_style = "plain" } - `cwd`: working directory for the process. Relative paths are resolved from `root`. - `autostart`: whether to start the process before startup workflows. -- `restart`: one of `never`, `on_failure`, or `always`. +- `restart`: one of `never`, `on_failure`, or `always`. Defaults to + `never`, so a process without this key is not automatically restarted. - `env`: extra environment variables for the process. - `readiness`: optional readiness probe. - `liveness`: optional liveness probe. diff --git a/fixtures/ci-smoke/devloop.toml b/fixtures/ci-smoke/devloop.toml index 19519d5..adfb67f 100644 --- a/fixtures/ci-smoke/devloop.toml +++ b/fixtures/ci-smoke/devloop.toml @@ -8,15 +8,15 @@ paths = ["watched.txt"] workflow = "content" [process.server] -command = ["python3", "-m", "http.server", "18081", "--bind", "127.0.0.1"] +command = ["./scripts/emit-ready.sh"] cwd = "." autostart = false -restart = "never" -output = { inherit = false } +restart = "always" +output = { inherit = true, rules = [{ state_key = "server_ready", pattern = "^(ready)$", extract = "regex" }] } [process.server.readiness] -kind = "http" -url = "http://127.0.0.1:18081/" +kind = "state_key" +key = "server_ready" interval_ms = 100 timeout_ms = 5000 @@ -38,6 +38,6 @@ steps = [ [workflow.content] steps = [ { action = "run_hook", hook = "current_value" }, - { action = "write_state", key = "current_url", value = "http://127.0.0.1:18081/{{current_value}}" }, + { action = "write_state", key = "current_url", value = "devloop://{{current_value}}" }, { action = "log", message = "changed value: {{current_value}}" }, ] diff --git a/fixtures/ci-smoke/scripts/emit-ready.sh b/fixtures/ci-smoke/scripts/emit-ready.sh new file mode 100644 index 0000000..ce68e35 --- /dev/null +++ b/fixtures/ci-smoke/scripts/emit-ready.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +printf 'ready\n' +exec tail -f /dev/null diff --git a/scripts/ci-smoke.sh b/scripts/ci-smoke.sh index 345a61e..3f4de43 100755 --- a/scripts/ci-smoke.sh +++ b/scripts/ci-smoke.sh @@ -64,17 +64,7 @@ trap dump_log ERR cp -R "${fixture_src}/." "${tmp_dir}/" chmod +x "${tmp_dir}/scripts/read-watched.sh" -smoke_port="$( - python3 - <<'PY' -import socket - -with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.bind(("127.0.0.1", 0)) - print(sock.getsockname()[1]) -PY -)" -sed -i.bak "s/18081/${smoke_port}/g" "${tmp_dir}/devloop.toml" -rm -f "${tmp_dir}/devloop.toml.bak" +chmod +x "${tmp_dir}/scripts/emit-ready.sh" state_path="${tmp_dir}/.devloop/state.json" devloop_bin="${repo_root}/target/debug/devloop" @@ -118,7 +108,7 @@ while time.time() < deadline: raise SystemExit("timed out waiting for watcher startup") PY -python3 - "$state_path" "$log_path" "${tmp_dir}/watched.txt" "${smoke_port}" <<'PY' +python3 - "$state_path" "$log_path" "${tmp_dir}/watched.txt" <<'PY' import json import pathlib import sys @@ -127,7 +117,6 @@ import time state_path = pathlib.Path(sys.argv[1]) log_path = pathlib.Path(sys.argv[2]) watched_path = pathlib.Path(sys.argv[3]) -smoke_port = sys.argv[4] deadline = time.time() + 15 next_write = 0.0 while time.time() < deadline: @@ -139,7 +128,7 @@ while time.time() < deadline: data = json.loads(state_path.read_text()) if ( data.get("current_value") == "updated" - and data.get("current_url") == f"http://127.0.0.1:{smoke_port}/updated" + and data.get("current_url") == "devloop://updated" ): if "changed value: updated" in log_path.read_text(): sys.exit(0)