chore: narrow the shared helper to .env.local writing

Slot assignment goes back into worktree-env: it had one caller, and the herdr plugin it was extracted for provisions worktrees before the checkout is usable, so it carries its own copy rather than sourcing one whose contents depend on whichever branch the main checkout sits on.

Writing .env.local stays shared - worktree-env and post-create.sh each had the same eleven lines.

Also corrects AGENTS.md: .env.local is a shared format rather than a shared function, and WM_DB_NAME comes from it rather than webmux's runtime.env.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-08-24 16:33:31 +02:00
parent af9dcf4976
commit 7a8cd3b5c6
3 changed files with 51 additions and 52 deletions
+11 -11
View File
@@ -64,9 +64,9 @@ they set disjoint environment variables, so check which one you are in:
See `backend/AGENTS.md` to restart the backend with different cargo features. Under herdr, `herdr
--skill` prints a full reference for inspecting and driving panes and agents, and the plugins that
provision worktrees live in `windmill-labs/windmill-herdr` — clone it and run `./setup.sh` to
install them and the keybindings they need. The commands below are for a plain checkout with
nothing running.
provision worktrees live in the private `windmill-labs/windmill-herdr` — clone it and run
`./setup.sh` to install them and the keybindings they need. The commands below are for a plain
checkout with nothing running.
- **Backend**: `cargo run` from `backend/` (API at http://localhost:8000)
- **Frontend**: `REMOTE=http://localhost:8000 npm run dev` from `frontend/` (port 3000+)
@@ -78,9 +78,8 @@ nothing running.
### Per-worktree ports and database
**`.env.local` in the worktree root is the portable answer** — `BACKEND_PORT`,
`FRONTEND_PORT`, `REMOTE`, `DATABASE_URL`, `CARGO_FEATURES`, `WM_DB_NAME`. Both managers write it
through the same helper (`wm_write_env_local` in `scripts/worktree-common.sh`), so it is correct
whichever one you are in, and it is readable despite the name.
`FRONTEND_PORT`, `REMOTE`, `DATABASE_URL`, `CARGO_FEATURES`, `WM_DB_NAME`. Both managers write
those fields, so it is correct whichever one you are in, and it is readable despite the name.
webmux additionally writes `$(git rev-parse --git-dir)/webmux/runtime.env`, which every pane
sources at startup and which carries the extras `.env.local` lacks: `WEBMUX_*`, `WM_CLONE_DB`,
@@ -98,11 +97,12 @@ opt-in via `WM_CLONE_DB` (in `.webmux.yaml` under webmux, or the `windmill.workt
The database is named after the **worktree directory, not the branch** (`scripts/worktree-common.sh`):
`windmill_` + the directory basename with `-``_`, which Postgres then truncates at 63
characters. Branch `hugo/win-2340-ai-agent-evals-standalone-agent-runs-and-eval-datasets` sits in
a worktree directory named `win-2340-…`, so its database is
`windmill_win_2340_ai_agent_evals_standalone_agent_runs_and_eval` — no `hugo_`, and the tail
chopped. Take `WM_DB_NAME` from `runtime.env` instead of reconstructing the name. Read those, or
discover from what is already running:
characters. The two managers lay worktrees out differently, so the same branch lands in different
directories and therefore different databases: under webmux, branch
`hugo/win-2340-ai-agent-evals-standalone-agent-runs-and-eval-datasets` sits in a directory named
`win-2340-…`, so its database is `windmill_win_2340_ai_agent_evals_standalone_agent_runs_and_eval`
— no `hugo_`, and the tail chopped. Take `WM_DB_NAME` from `.env.local` instead of reconstructing
the name. Read that, or discover from what is already running:
```bash
psql postgres://postgres:changeme@localhost:5432/postgres -tAc \
+2 -40
View File
@@ -13,46 +13,8 @@ wm_main_repo_root() {
cd "$(git -C "$repo_root" rev-parse --git-common-dir 2>/dev/null)/.." && pwd
}
wm_port_in_use() {
lsof -nP -iTCP:"$1" -sTCP:LISTEN &>/dev/null
}
# Prints "<backend_port> <frontend_port>" for the lowest free slot. Slots are read from
# each worktree's .env.local rather than from position in `git worktree list`: removing a
# worktree in the middle would otherwise hand its slot to a new one while a live
# neighbour still listens on those ports. Slot 0 belongs to the main checkout.
wm_assign_ports() {
local repo_root=$1
local slot=${WM_SLOT:-}
local used_slots=() wt_path backend_port frontend_port bp
if [[ -z "$slot" ]]; then
while IFS= read -r wt_path; do
[[ "$wt_path" == "$repo_root" ]] && continue
[[ -f "$wt_path/.env.local" ]] || continue
bp=$(grep '^BACKEND_PORT=' "$wt_path/.env.local" | cut -d= -f2 || true)
if [[ -n "$bp" && "$bp" -gt 8000 ]]; then
used_slots+=("$(( (bp - 8000) / 10 ))")
fi
done < <(git -C "$repo_root" worktree list --porcelain | sed -n 's/^worktree //p')
slot=1
while [[ " ${used_slots[*]:-} " == *" $slot "* ]]; do
((slot++))
done
echo "Auto-assigned slot $slot (used: ${used_slots[*]:-none})" >&2
fi
backend_port=$((8000 + slot * 10))
frontend_port=$((3000 + slot * 10))
if wm_port_in_use "$backend_port" || wm_port_in_use "$frontend_port"; then
echo "WARNING: slot $slot ports ($backend_port/$frontend_port) already in use" >&2
fi
printf '%s %s\n' "$backend_port" "$frontend_port"
}
# Written by both worktree entry points — `worktree-env` by hand, `post-create.sh` from the
# webmux hook — and read by agents to find their own ports, so the two must not drift.
wm_write_env_local() {
local repo_root=$1
local backend_port=$2
+38 -1
View File
@@ -3,6 +3,43 @@ set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/worktree-common.sh"
read -r backend_port frontend_port < <(wm_assign_ports "$(pwd)")
port_in_use() {
lsof -nP -iTCP:"$1" -sTCP:LISTEN &>/dev/null
}
if [[ -z "${WM_SLOT:-}" ]]; then
# Scan .env.local files of existing worktrees to find which slots are claimed,
# then pick the lowest free slot. This avoids collisions when worktrees are
# removed and new ones created (position-based indexing would re-use slots
# still held by surviving worktrees).
used_slots=()
current_dir="$(pwd)"
while IFS= read -r wt_path; do
[[ "$wt_path" == "$current_dir" ]] && continue
if [[ -f "$wt_path/.env.local" ]]; then
bp=$(grep '^BACKEND_PORT=' "$wt_path/.env.local" | cut -d= -f2 || true)
if [[ -n "$bp" && "$bp" -gt 8000 ]]; then
used_slots+=("$(( (bp - 8000) / 10 ))")
fi
fi
done < <(git worktree list --porcelain | sed -n 's/^worktree //p')
# Find lowest available slot (slot 0 = 8000/3000 is reserved for main)
WM_SLOT=1
while [[ " ${used_slots[*]:-} " == *" $WM_SLOT "* ]]; do
((WM_SLOT++))
done
echo "Auto-assigned slot $WM_SLOT (used: ${used_slots[*]:-none})"
fi
# Slot-based: predictable ports for SSH forwarding
# Slot 0 = 8000/3000, slot 1 = 8010/3010, slot 2 = 8020/3020, etc.
backend_port=$((8000 + WM_SLOT * 10))
frontend_port=$((3000 + WM_SLOT * 10))
if port_in_use "$backend_port" || port_in_use "$frontend_port"; then
echo "WARNING: Slot $WM_SLOT ports ($backend_port/$frontend_port) already in use" >&2
fi
wm_write_env_local "$(pwd)" "$backend_port" "$frontend_port"
wm_shared_post_create "$(pwd)"