mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
4e29b3b125
The slot scan and the .env.local writer existed twice, once inline in scripts/worktree-env and once in scripts/post-create.sh. Both are now wm_assign_ports and wm_write_env_local in scripts/worktree-common.sh, so a third caller cannot drift from the other two. wm_assign_ports takes the worktree root instead of reading the process cwd, which lets it run from outside the worktree, and prints its slot line to stderr so stdout carries only the port pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
372 lines
12 KiB
Bash
Executable File
372 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
wm_is_true() {
|
|
case "${1:-}" in
|
|
1|true|TRUE|yes|YES|on|ON) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
wm_main_repo_root() {
|
|
local repo_root=${1:-.}
|
|
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"
|
|
}
|
|
|
|
wm_write_env_local() {
|
|
local repo_root=$1
|
|
local backend_port=$2
|
|
local frontend_port=$3
|
|
local env_file="${repo_root}/.env.local"
|
|
|
|
cat > "$env_file" <<EOF
|
|
BACKEND_PORT=$backend_port
|
|
FRONTEND_PORT=$frontend_port
|
|
REMOTE=http://localhost:$backend_port
|
|
EOF
|
|
|
|
if [[ -n "${CARGO_FEATURES:-}" ]]; then
|
|
echo "CARGO_FEATURES=$CARGO_FEATURES" >> "$env_file"
|
|
fi
|
|
|
|
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
|
|
}
|
|
|
|
wm_setup_database() {
|
|
local repo_root=$1
|
|
local env_file=$2
|
|
local wt_basename db_name db_conn db_url license_key
|
|
|
|
wt_basename="$(basename "$repo_root")"
|
|
db_name="windmill_${wt_basename//-/_}"
|
|
db_conn="postgres://postgres:changeme@127.0.0.1:5432"
|
|
|
|
if ! command -v psql >/dev/null 2>&1; then
|
|
echo "WARNING: psql not found, skipping per-worktree database creation" >&2
|
|
return
|
|
fi
|
|
|
|
if psql "$db_conn/postgres" -tc "SELECT 1 FROM pg_database WHERE datname = '${db_name}'" 2>/dev/null | grep -q 1; then
|
|
echo "Database $db_name already exists"
|
|
else
|
|
if wm_is_true "${WM_CLONE_DB:-}"; then
|
|
psql "$db_conn/postgres" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'windmill' AND pid <> pg_backend_pid();" 2>/dev/null || true
|
|
psql "$db_conn/postgres" -c "CREATE DATABASE ${db_name} TEMPLATE windmill" 2>/dev/null \
|
|
&& echo "Created database $db_name (template: windmill)" \
|
|
|| echo "WARNING: Could not create database $db_name from template windmill" >&2
|
|
else
|
|
psql "$db_conn/postgres" -c "CREATE DATABASE ${db_name}" 2>/dev/null \
|
|
&& echo "Created database $db_name" \
|
|
|| echo "WARNING: Could not create database $db_name (is PostgreSQL running?)" >&2
|
|
fi
|
|
fi
|
|
|
|
db_url="${db_conn}/${db_name}?sslmode=disable"
|
|
DATABASE_URL="$db_url" sqlx migrate run --source "${repo_root}/backend/migrations" \
|
|
&& echo "Migrations applied to $db_name" \
|
|
|| echo "WARNING: Could not run migrations on $db_name" >&2
|
|
|
|
license_key="$(psql "$db_conn/windmill" -t -A -c "SELECT value FROM global_settings WHERE name = 'license_key'" 2>/dev/null || true)"
|
|
if [[ -n "$license_key" ]]; then
|
|
psql "$db_url" -c "INSERT INTO global_settings (name, value) VALUES ('license_key', '${license_key}'::jsonb) ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value" 2>/dev/null \
|
|
&& echo "Copied license_key to $db_name" \
|
|
|| echo "WARNING: Could not copy license_key to $db_name" >&2
|
|
fi
|
|
|
|
cat >> "$env_file" <<EOF
|
|
export DATABASE_URL=$db_url
|
|
WM_DB_NAME=$db_name
|
|
EOF
|
|
echo "Added DATABASE_URL for database $db_name to .env.local"
|
|
}
|
|
|
|
wm_copy_dependencies() {
|
|
local repo_root=$1
|
|
local main_repo_root=$2
|
|
|
|
if [[ -f "${main_repo_root}/backend/.env" ]]; then
|
|
cp "${main_repo_root}/backend/.env" "${repo_root}/backend/"
|
|
echo "Copied backend/.env"
|
|
fi
|
|
|
|
if [[ -d "${main_repo_root}/frontend/node_modules" ]]; then
|
|
cp -a "${main_repo_root}/frontend/node_modules" "${repo_root}/frontend/"
|
|
echo "Copied frontend/node_modules (with symlinks preserved)"
|
|
fi
|
|
|
|
if [[ -d "${main_repo_root}/cli/node_modules" ]]; then
|
|
cp -a "${main_repo_root}/cli/node_modules" "${repo_root}/cli/"
|
|
echo "Copied cli/node_modules (with symlinks preserved)"
|
|
fi
|
|
|
|
if [[ -d "${repo_root}/cli" ]]; then
|
|
(cd "${repo_root}/cli" && npm install && npm run gen-client) \
|
|
&& echo "CLI deps installed and client generated" \
|
|
|| echo "WARNING: CLI setup failed" >&2
|
|
fi
|
|
|
|
local nav_bin="${main_repo_root}/wm-ts-nav/target/release/wm-ts-nav"
|
|
if [[ -f "$nav_bin" ]]; then
|
|
mkdir -p "${repo_root}/wm-ts-nav/target/release"
|
|
cp "$nav_bin" "${repo_root}/wm-ts-nav/target/release/"
|
|
echo "Copied wm-ts-nav binary"
|
|
fi
|
|
}
|
|
|
|
wm_copy_optional_env_files() {
|
|
local repo_root=$1
|
|
local main_repo_root=$2
|
|
local rel_path src_env dst_dir
|
|
|
|
for rel_path in "integration_tests/ai_agent_tests" "ai_evals"; do
|
|
src_env="${main_repo_root}/${rel_path}/.env"
|
|
dst_dir="${repo_root}/${rel_path}"
|
|
if [[ -f "$src_env" ]]; then
|
|
mkdir -p "$dst_dir"
|
|
cp "$src_env" "${dst_dir}/.env"
|
|
echo "Copied ${rel_path}/.env"
|
|
fi
|
|
done
|
|
}
|
|
|
|
wm_allow_direnv() {
|
|
local repo_root=$1
|
|
if command -v direnv >/dev/null 2>&1 && [[ -f "${repo_root}/.envrc" ]]; then
|
|
(cd "$repo_root" && direnv allow)
|
|
echo "direnv allowed"
|
|
fi
|
|
}
|
|
|
|
wm_trust_claude() {
|
|
local repo_root=$1
|
|
local claude_json="${HOME}/.claude.json"
|
|
|
|
if [[ ! -f "$claude_json" ]] || ! command -v python3 >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
|
|
REPO_ROOT="$repo_root" CLAUDE_JSON="$claude_json" python3 - <<'PY' \
|
|
&& echo "Added $repo_root to Claude Code trusted directories" \
|
|
|| echo "Warning: Could not update Claude Code trusted directories"
|
|
import json
|
|
import os
|
|
|
|
path = os.environ["REPO_ROOT"]
|
|
claude_json = os.environ["CLAUDE_JSON"]
|
|
with open(claude_json, "r") as f:
|
|
data = json.load(f)
|
|
projects = data.setdefault("projects", {})
|
|
proj = projects.setdefault(path, {})
|
|
proj["hasTrustDialogAccepted"] = True
|
|
proj["hasCompletedProjectOnboarding"] = True
|
|
with open(claude_json, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
PY
|
|
}
|
|
|
|
wm_find_ee_repo() {
|
|
local repo_root=$1
|
|
local main_repo_root=$2
|
|
local candidate
|
|
|
|
for candidate in \
|
|
"${main_repo_root}/../windmill-ee-private" \
|
|
"${repo_root}/../windmill-ee-private" \
|
|
"${HOME}/windmill-ee-private" \
|
|
"${HOME}/projects/windmill-ee-private"; do
|
|
if [[ -d "$candidate" ]]; then
|
|
cd "$candidate" && pwd
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# The EE commit this CE checkout builds against. CI reads the same file, so basing a new EE
|
|
# worktree on it keeps a local `cargo check --features private` on the tree CI compiles. Local
|
|
# `main` in the EE repo is not a substitute: nothing fast-forwards it, so it drifts behind the pin.
|
|
wm_ee_pinned_ref() {
|
|
local repo_root=$1 ref
|
|
ref="$(tr -d '[:space:]' < "${repo_root}/backend/ee-repo-ref.txt" 2>/dev/null)" || return 1
|
|
[[ -n "$ref" ]] || return 1
|
|
printf '%s' "$ref"
|
|
}
|
|
|
|
wm_setup_ee_worktree() {
|
|
local repo_root=$1
|
|
local main_repo_root=$2
|
|
local ee_repo branch wt_basename ee_worktree_dir ee_rel rust_plugin ee_ref
|
|
|
|
if ! ee_repo="$(wm_find_ee_repo "$repo_root" "$main_repo_root")"; then
|
|
return
|
|
fi
|
|
|
|
branch="$(git -C "$repo_root" branch --show-current 2>/dev/null || true)"
|
|
wt_basename="$(basename "$repo_root")"
|
|
ee_worktree_dir="${ee_repo}__worktrees/${wt_basename}"
|
|
|
|
if [[ -n "$branch" && ! -d "$ee_worktree_dir" ]]; then
|
|
mkdir -p "$(dirname "$ee_worktree_dir")"
|
|
git -C "$ee_repo" fetch --quiet 2>/dev/null || true
|
|
|
|
# Guard on refs/heads: without it, `worktree add` DWIMs a remote-only branch
|
|
# into a new local branch here, skipping the explicit --track arm below.
|
|
if git -C "$ee_repo" show-ref --verify --quiet "refs/heads/$branch" \
|
|
&& git -C "$ee_repo" worktree add "$ee_worktree_dir" "$branch" 2>/dev/null; then
|
|
echo "Created EE worktree at $ee_worktree_dir (existing local branch: $branch)"
|
|
elif git -C "$ee_repo" show-ref --verify --quiet "refs/remotes/origin/$branch" \
|
|
&& git -C "$ee_repo" worktree add --track -b "$branch" "$ee_worktree_dir" "origin/$branch" 2>/dev/null; then
|
|
echo "Created EE worktree at $ee_worktree_dir (tracking origin/$branch)"
|
|
elif ee_ref="$(wm_ee_pinned_ref "$repo_root")" \
|
|
&& git -C "$ee_repo" worktree add -b "$branch" "$ee_worktree_dir" "$ee_ref" 2>/dev/null; then
|
|
echo "Created EE worktree at $ee_worktree_dir (new branch: $branch from pinned ${ee_ref:0:12})"
|
|
elif git -C "$ee_repo" worktree add -b "$branch" "$ee_worktree_dir" main 2>/dev/null; then
|
|
echo "Created EE worktree at $ee_worktree_dir (new branch: $branch from main — pin unavailable)"
|
|
else
|
|
echo "Warning: Could not create EE worktree for branch $branch"
|
|
fi
|
|
elif [[ -d "$ee_worktree_dir" ]]; then
|
|
echo "EE worktree already exists at $ee_worktree_dir"
|
|
fi
|
|
|
|
if [[ ! -d "$ee_worktree_dir" ]]; then
|
|
return
|
|
fi
|
|
|
|
ee_rel="$(REPO_ROOT="$repo_root" EE_WORKTREE_DIR="$ee_worktree_dir" python3 - <<'PY' 2>/dev/null || echo "$ee_worktree_dir"
|
|
import os
|
|
print(os.path.relpath(os.environ["EE_WORKTREE_DIR"], os.environ["REPO_ROOT"]))
|
|
PY
|
|
)"
|
|
mkdir -p "${repo_root}/.claude"
|
|
rust_plugin=""
|
|
if wm_is_true "${USE_RUST_PLUGIN:-}"; then
|
|
rust_plugin=',
|
|
"enabledPlugins": {
|
|
"rust-analyzer-lsp@claude-plugins-official": true
|
|
}'
|
|
fi
|
|
cat > "${repo_root}/.claude/settings.local.json" <<EOF
|
|
{
|
|
"permissions": {
|
|
"additionalDirectories": [
|
|
"$ee_rel"
|
|
]
|
|
}${rust_plugin}
|
|
}
|
|
EOF
|
|
echo "Created .claude/settings.local.json with EE path: $ee_rel"
|
|
|
|
if [[ -x "${repo_root}/backend/substitute_ee_code.sh" ]]; then
|
|
"${repo_root}/backend/substitute_ee_code.sh" -d "$ee_worktree_dir"
|
|
fi
|
|
}
|
|
|
|
wm_shared_post_create() {
|
|
local repo_root=$1
|
|
local main_repo_root
|
|
|
|
main_repo_root="$(wm_main_repo_root "$repo_root")"
|
|
wm_allow_direnv "$repo_root"
|
|
wm_setup_database "$repo_root" "${repo_root}/.env.local"
|
|
wm_copy_dependencies "$repo_root" "$main_repo_root"
|
|
wm_copy_optional_env_files "$repo_root" "$main_repo_root"
|
|
wm_trust_claude "$repo_root"
|
|
wm_setup_ee_worktree "$repo_root" "$main_repo_root"
|
|
}
|
|
|
|
wm_kill_processes_from_env_file() {
|
|
local env_file=$1
|
|
local pid port
|
|
|
|
if [[ ! -f "$env_file" ]]; then
|
|
return
|
|
fi
|
|
|
|
# shellcheck disable=SC1090
|
|
source "$env_file"
|
|
for port in "${BACKEND_PORT:-}" "${FRONTEND_PORT:-}"; do
|
|
[[ -z "$port" ]] && continue
|
|
pid="$(lsof -ti "TCP:${port}" -sTCP:LISTEN 2>/dev/null || true)"
|
|
if [[ -n "$pid" ]]; then
|
|
kill "$pid" 2>/dev/null && echo "Killed process $pid on port $port" \
|
|
|| echo "Warning: Could not kill process $pid on port $port"
|
|
fi
|
|
done
|
|
}
|
|
|
|
wm_shared_pre_remove() {
|
|
local repo_root=$1
|
|
local env_file="${repo_root}/.env.local"
|
|
local db_conn wt_basename main_repo_root ee_repo ee_worktree_dir
|
|
|
|
if [[ -f "$env_file" ]]; then
|
|
# shellcheck disable=SC1090
|
|
source "$env_file"
|
|
fi
|
|
|
|
if [[ -n "${WM_DB_NAME:-}" ]]; then
|
|
db_conn="postgres://postgres:changeme@127.0.0.1:5432"
|
|
if command -v psql >/dev/null 2>&1; then
|
|
psql "$db_conn/postgres" -c "DROP DATABASE IF EXISTS ${WM_DB_NAME} WITH (FORCE)" 2>/dev/null \
|
|
&& echo "Dropped database $WM_DB_NAME" \
|
|
|| echo "Warning: Could not drop database $WM_DB_NAME"
|
|
else
|
|
echo "psql not found, skipping database cleanup for $WM_DB_NAME"
|
|
fi
|
|
fi
|
|
|
|
main_repo_root="$(wm_main_repo_root "$repo_root")"
|
|
wt_basename="$(basename "$repo_root")"
|
|
if ee_repo="$(wm_find_ee_repo "$repo_root" "$main_repo_root")"; then
|
|
ee_worktree_dir="${ee_repo}__worktrees/${wt_basename}"
|
|
if [[ -d "$ee_worktree_dir" ]]; then
|
|
git -C "$ee_repo" worktree remove "$ee_worktree_dir" --force 2>/dev/null \
|
|
&& echo "Removed EE worktree at $ee_worktree_dir" \
|
|
|| echo "Warning: Could not remove EE worktree at $ee_worktree_dir"
|
|
fi
|
|
fi
|
|
|
|
}
|