mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-01 08:59:46 +02:00
chore: add Conductor per-worktree dev setup (.conductor/ + .worktreeinclude)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0a6f7673b5
commit
c099dad03d
10 changed files with 352 additions and 0 deletions
81
.conductor/README.md
Normal file
81
.conductor/README.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Conductor workspace setup
|
||||
|
||||
This directory makes [Conductor](https://conductor.build) workspaces (git
|
||||
worktrees) self-contained: each one installs its own deps and runs its own
|
||||
backend + UI on a dedicated port range, so you can develop several branches at
|
||||
once without collisions.
|
||||
|
||||
## How it works
|
||||
|
||||
Conductor gives every workspace a block of **10 ports** starting at
|
||||
`$CONDUCTOR_PORT`. We use:
|
||||
|
||||
| Port | Service | Script |
|
||||
| --------------- | ------------------ | ----------------------- |
|
||||
| `CONDUCTOR_PORT` | Backend (uvicorn) | `run-backend.sh` |
|
||||
| `CONDUCTOR_PORT + 1` | UI (Next.js) | `run-ui.sh` |
|
||||
| `+2 .. +9` | reserved | — |
|
||||
|
||||
The UI is wired to its own workspace's backend (`NEXT_PUBLIC_BACKEND_URL`,
|
||||
`BACKEND_URL`) and the backend's CORS / `UI_APP_URL` point back at its own UI —
|
||||
all derived from `$CONDUCTOR_PORT`, so no two workspaces interfere.
|
||||
|
||||
### Shared, NOT per-workspace
|
||||
|
||||
Postgres, Redis, and MinIO run as a **single shared Docker stack** (compose
|
||||
project `dograh`). `setup.sh` brings it up idempotently with
|
||||
`COMPOSE_PROJECT_NAME=dograh` so every workspace reuses the same one instead of
|
||||
fighting over the fixed 5432/6379/9000 ports. All workspaces therefore share one
|
||||
database — fine for app servers, but it means the **Arq worker should run in only
|
||||
one workspace** (`run-worker.sh`), since a single worker drains the shared queue
|
||||
for everyone.
|
||||
|
||||
## The Run menu
|
||||
|
||||
Conductor shows three buttons (`run_mode = "concurrent"`, so they coexist):
|
||||
|
||||
- **backend** (default) — FastAPI/uvicorn with `--reload` on `$CONDUCTOR_PORT`
|
||||
- **ui** — Next.js dev server on `$CONDUCTOR_PORT + 1`
|
||||
- **worker** — Arq worker; start in just one workspace
|
||||
|
||||
Start **backend** and **ui** in each workspace you want to use. Start **worker**
|
||||
once (e.g. in your primary workspace) when you need background jobs — the same
|
||||
way you used to run a single arq worker in VS Code.
|
||||
|
||||
## Proving you're in the right worktree
|
||||
|
||||
`run-ui.sh` exports `NEXT_PUBLIC_WORKSPACE_NAME=$CONDUCTOR_WORKSPACE_NAME`, which
|
||||
`ui/src/components/WorkspaceBadge.tsx` renders as a small color-coded pill in the
|
||||
bottom-left corner (e.g. `⬡ pattaya :8201`). The color is derived from the
|
||||
workspace name, so two worktrees are instantly distinguishable. The badge is
|
||||
invisible in production and in a plain `npm run dev` (no env var set).
|
||||
|
||||
## Creating a new workspace
|
||||
|
||||
In Conductor: **New Workspace** → pick a branch. Conductor first copies the
|
||||
gitignored env files listed in `../.worktreeinclude` (`api/.env`, `ui/.env`,
|
||||
`ui/.env.local`, …) from your main checkout, then `setup.sh` runs automatically
|
||||
and:
|
||||
|
||||
1. checks out the `pipecat` submodule,
|
||||
2. builds `venv` (Python 3.13) + installs backend/pipecat deps,
|
||||
3. `npm install` for the UI,
|
||||
4. ensures the shared Docker stack is up,
|
||||
5. runs `alembic upgrade head`.
|
||||
|
||||
The first setup is slow (deps); afterwards the Run buttons are instant.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------- | -------------------------------------------------- |
|
||||
| `settings.toml` | Conductor config: setup + run scripts, run_mode |
|
||||
| `setup.sh` | One-time workspace bootstrap |
|
||||
| `run-backend.sh` | Foreground uvicorn on `$CONDUCTOR_PORT` |
|
||||
| `run-ui.sh` | Foreground Next.js on `$CONDUCTOR_PORT + 1` |
|
||||
| `run-worker.sh` | Foreground Arq worker (run in one workspace only) |
|
||||
| `../.worktreeinclude` | Gitignored env files Conductor copies per workspace |
|
||||
|
||||
> Need machine-local tweaks (e.g. a different port base or skipping the worker)?
|
||||
> Put them in `.conductor/settings.local.toml`, which is personal and not
|
||||
> committed.
|
||||
30
.conductor/run-backend.sh
Executable file
30
.conductor/run-backend.sh
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env bash
|
||||
# Conductor run script — Backend (FastAPI/uvicorn) for THIS workspace.
|
||||
#
|
||||
# Binds $CONDUCTOR_PORT and points CORS / UI_APP_URL at this workspace's UI
|
||||
# ($CONDUCTOR_PORT + 1). Runs in the FOREGROUND with exec (no &) so Conductor's
|
||||
# SIGHUP cleanly tears it down.
|
||||
set -euo pipefail
|
||||
cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}"
|
||||
|
||||
PORT="${CONDUCTOR_PORT:-8000}"
|
||||
UI_PORT="$((PORT + 1))"
|
||||
|
||||
# Load the workspace's api/.env, then override the port-specific bits. The
|
||||
# backend reads config via os.getenv, and these exports happen after the source,
|
||||
# so they win over the values copied from the main checkout (which assume 8000).
|
||||
if [[ -f api/.env ]]; then set -a; # shellcheck disable=SC1091
|
||||
source api/.env; set +a; fi
|
||||
export FASTAPI_PORT="$PORT"
|
||||
export UI_APP_URL="http://localhost:${UI_PORT}"
|
||||
export CORS_ALLOWED_ORIGINS="http://localhost:${UI_PORT},http://127.0.0.1:${UI_PORT}"
|
||||
|
||||
if [[ ! -d venv ]]; then
|
||||
echo "ERROR: venv missing. Re-run workspace setup (.conductor/setup.sh)." >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source venv/bin/activate
|
||||
|
||||
echo "[backend] workspace=${CONDUCTOR_WORKSPACE_NAME:-?} port=${PORT} ui=${UI_PORT}"
|
||||
exec uvicorn api.app:app --host 0.0.0.0 --port "$PORT" --reload --reload-dir api
|
||||
31
.conductor/run-ui.sh
Executable file
31
.conductor/run-ui.sh
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env bash
|
||||
# Conductor run script — UI (Next.js) for THIS workspace.
|
||||
#
|
||||
# Binds $CONDUCTOR_PORT + 1, talks to this workspace's backend on $CONDUCTOR_PORT,
|
||||
# and tags the build with the workspace identity (NEXT_PUBLIC_WORKSPACE_NAME) so
|
||||
# the in-app WorkspaceBadge shows which worktree you're looking at.
|
||||
# Runs in the FOREGROUND with exec (no &) so Conductor can stop it cleanly.
|
||||
set -euo pipefail
|
||||
cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}"
|
||||
|
||||
PORT="${CONDUCTOR_PORT:-8000}"
|
||||
UI_PORT="$((PORT + 1))"
|
||||
BACKEND="http://localhost:${PORT}"
|
||||
|
||||
# Ensure node is on PATH (load nvm + honor .nvmrc if needed).
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||
# shellcheck disable=SC1091
|
||||
[[ -s "$NVM_DIR/nvm.sh" ]] && . "$NVM_DIR/nvm.sh"
|
||||
command -v nvm >/dev/null 2>&1 && nvm use >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Shell env overrides .env files in Next.js, so this points the UI at the right
|
||||
# backend and stamps the workspace name into the client bundle.
|
||||
export BACKEND_URL="$BACKEND"
|
||||
export NEXT_PUBLIC_BACKEND_URL="$BACKEND"
|
||||
export NEXT_PUBLIC_WORKSPACE_NAME="${CONDUCTOR_WORKSPACE_NAME:-local}"
|
||||
|
||||
cd ui
|
||||
echo "[ui] workspace=${NEXT_PUBLIC_WORKSPACE_NAME} port=${UI_PORT} backend=${BACKEND}"
|
||||
exec npm run dev -- --port "$UI_PORT"
|
||||
21
.conductor/run-worker.sh
Executable file
21
.conductor/run-worker.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
# Conductor run script — Arq background worker.
|
||||
#
|
||||
# Run this in ONE workspace only. Every workspace shares the same Redis/Postgres,
|
||||
# so a single arq worker drains the task queue for all of them — multiple workers
|
||||
# would just fight over the same jobs. Foreground exec so Conductor stops it cleanly.
|
||||
set -euo pipefail
|
||||
cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}"
|
||||
|
||||
if [[ -f api/.env ]]; then set -a; # shellcheck disable=SC1091
|
||||
source api/.env; set +a; fi
|
||||
|
||||
if [[ ! -d venv ]]; then
|
||||
echo "ERROR: venv missing. Re-run workspace setup (.conductor/setup.sh)." >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source venv/bin/activate
|
||||
|
||||
echo "[worker] arq worker on shared Redis — workspace=${CONDUCTOR_WORKSPACE_NAME:-?}"
|
||||
exec python -m arq api.tasks.arq.WorkerSettings --custom-log-dict api.tasks.arq.LOG_CONFIG
|
||||
40
.conductor/settings.toml
Normal file
40
.conductor/settings.toml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Conductor project config — shared, committed so every contributor (and every
|
||||
# new workspace) gets the same per-worktree dev workflow.
|
||||
#
|
||||
# Model: each Conductor workspace is its own git worktree and gets its own
|
||||
# 10-port range starting at $CONDUCTOR_PORT. We use:
|
||||
# $CONDUCTOR_PORT -> backend (FastAPI/uvicorn)
|
||||
# $CONDUCTOR_PORT + 1 -> ui (Next.js)
|
||||
# +2 .. +9 -> reserved for future per-workspace services
|
||||
#
|
||||
# Postgres/Redis/MinIO are a single SHARED Docker stack (project name "dograh"),
|
||||
# not per-workspace — see .conductor/setup.sh and .conductor/README.md.
|
||||
"$schema" = "https://conductor.build/schemas/settings.repo.schema.json"
|
||||
|
||||
[scripts]
|
||||
# Runs once when a workspace is created: copies gitignored env files, inits the
|
||||
# pipecat submodule, builds the venv + node_modules, ensures the shared Docker
|
||||
# stack, and runs migrations.
|
||||
setup = "bash .conductor/setup.sh"
|
||||
|
||||
# concurrent => multiple workspaces can run their dev servers at the same time
|
||||
# (each on its own CONDUCTOR_PORT range), and within a workspace the backend +
|
||||
# ui + worker run buttons don't kill each other.
|
||||
run_mode = "concurrent"
|
||||
|
||||
# Backend (FastAPI/uvicorn) on $CONDUCTOR_PORT. Default Run button.
|
||||
[scripts.run.backend]
|
||||
command = "bash .conductor/run-backend.sh"
|
||||
icon = "server"
|
||||
default = true
|
||||
|
||||
# UI (Next.js) on $CONDUCTOR_PORT + 1, wired to this workspace's backend.
|
||||
[scripts.run.ui]
|
||||
command = "bash .conductor/run-ui.sh"
|
||||
icon = "play"
|
||||
|
||||
# Arq background worker. Start in ONE workspace only — all workspaces share the
|
||||
# same Redis/Postgres, so a single worker drains the queue for everyone.
|
||||
[scripts.run.worker]
|
||||
command = "bash .conductor/run-worker.sh"
|
||||
icon = "server"
|
||||
91
.conductor/setup.sh
Executable file
91
.conductor/setup.sh
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env bash
|
||||
# Conductor setup script — runs ONCE when a workspace (git worktree) is created.
|
||||
#
|
||||
# A fresh worktree only has git-tracked files, so this recreates the rest: the
|
||||
# pipecat submodule checkout, a Python venv, ui/node_modules, the shared local
|
||||
# Docker stack, and the DB schema. (Gitignored env files are copied separately
|
||||
# by .worktreeinclude, BEFORE this script runs.)
|
||||
#
|
||||
# Conductor injects: CONDUCTOR_ROOT_PATH (main checkout), CONDUCTOR_WORKSPACE_PATH,
|
||||
# CONDUCTOR_WORKSPACE_NAME, CONDUCTOR_PORT (first of 10), CONDUCTOR_IS_LOCAL.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${CONDUCTOR_ROOT_PATH:-}"
|
||||
WS="${CONDUCTOR_WORKSPACE_PATH:-$PWD}"
|
||||
cd "$WS"
|
||||
|
||||
log() { printf '\n\033[1;36m[conductor-setup]\033[0m %s\n' "$*"; }
|
||||
|
||||
# 1) Env files — normally copied by .worktreeinclude BEFORE this runs (the
|
||||
# Conductor-native file-copy mechanism). This block is only a FALLBACK so that
|
||||
# `bash .conductor/setup.sh` also works when run by hand outside Conductor: it
|
||||
# copies just the files that are still missing, and is a no-op under Conductor.
|
||||
if [[ -n "$ROOT" && "$ROOT" != "$WS" ]]; then
|
||||
for f in api/.env api/.env.test ui/.env ui/.env.local ui/.env.sentry-build-plugin; do
|
||||
if [[ ! -f "$f" && -f "$ROOT/$f" ]]; then
|
||||
mkdir -p "$(dirname "$f")"
|
||||
cp "$ROOT/$f" "$f"
|
||||
log "fallback-copied $f (not present from .worktreeinclude)"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# 2) pipecat submodule — REQUIRED here, NOT redundant with step 3.
|
||||
# A fresh git worktree has an empty pipecat/. setup_requirements.sh --dev
|
||||
# (step 3) deliberately SKIPS `git submodule update` (it assumes CI already
|
||||
# checked out submodules) but still runs `uv pip install -e ./pipecat`, which
|
||||
# fails unless the checkout is already on disk. So we populate it first.
|
||||
log "Initializing git submodules (pipecat)"
|
||||
git submodule update --init --recursive
|
||||
|
||||
# 3) Python venv with 3.13 + backend/pipecat deps ----------------------------
|
||||
# Bare `python3` may be 3.14 here; setup_requirements.sh requires 3.12/3.13.
|
||||
PY313="$(command -v python3.13 || true)"
|
||||
if [[ -z "$PY313" ]]; then
|
||||
echo "ERROR: python3.13 not found. Install it (e.g. brew install python@3.13)." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d venv ]]; then
|
||||
log "Creating venv with $PY313 ($("$PY313" --version))"
|
||||
"$PY313" -m venv venv
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source venv/bin/activate
|
||||
log "Installing backend + pipecat deps (uv) — this is the slow step"
|
||||
bash scripts/setup_requirements.sh --dev
|
||||
|
||||
# 4) UI deps -----------------------------------------------------------------
|
||||
ensure_node() {
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||
# shellcheck disable=SC1091
|
||||
[[ -s "$NVM_DIR/nvm.sh" ]] && . "$NVM_DIR/nvm.sh"
|
||||
command -v nvm >/dev/null 2>&1 && nvm use >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
ensure_node
|
||||
log "Installing UI deps (npm install)"
|
||||
( cd ui && npm install )
|
||||
|
||||
# 5) Shared local Docker stack (idempotent, pinned project name) -------------
|
||||
# All workspaces share ONE stack named "dograh" so they never collide on the
|
||||
# fixed Postgres/Redis/MinIO ports. If it's already up this is a no-op.
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
log "Ensuring shared Docker stack (COMPOSE_PROJECT_NAME=dograh)"
|
||||
COMPOSE_PROJECT_NAME=dograh docker compose -f docker-compose-local.yaml up -d \
|
||||
|| echo " (warning: could not start docker stack; start it manually)"
|
||||
else
|
||||
echo " (docker not found; start Postgres/Redis/MinIO yourself)"
|
||||
fi
|
||||
|
||||
# 6) DB migrations (best-effort; shared DB, alembic is idempotent) -----------
|
||||
if [[ -f api/.env ]]; then
|
||||
log "Running DB migrations (alembic upgrade head)"
|
||||
set -a; # shellcheck disable=SC1091
|
||||
source api/.env; set +a
|
||||
alembic -c api/alembic.ini upgrade head || echo " (warning: migrations skipped/failed — is the DB up?)"
|
||||
fi
|
||||
|
||||
PORT="${CONDUCTOR_PORT:-8000}"
|
||||
log "Setup complete for '${CONDUCTOR_WORKSPACE_NAME:-?}'."
|
||||
echo " Run menu: backend -> :${PORT} ui -> :$((PORT + 1)) worker -> shared arq"
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -4,6 +4,9 @@ __pycache__
|
|||
.env.prod
|
||||
.env.test
|
||||
|
||||
# Conductor personal/per-machine overrides (settings.toml IS committed)
|
||||
.conductor/settings.local.toml
|
||||
|
||||
# logs and run directory on production
|
||||
/logs/
|
||||
/run/
|
||||
|
|
|
|||
10
.worktreeinclude
Normal file
10
.worktreeinclude
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Conductor: gitignored files copied from the main checkout into every new
|
||||
# workspace (git worktree), BEFORE .conductor/setup.sh runs. These hold real
|
||||
# secrets and are .gitignored, so a fresh worktree wouldn't otherwise have them.
|
||||
# Patterns are gitignore-style. Adding this file overrides Conductor's default
|
||||
# ".env*" copy, so list everything local dev needs explicitly.
|
||||
api/.env
|
||||
api/.env.test
|
||||
ui/.env
|
||||
ui/.env.local
|
||||
ui/.env.sentry-build-plugin
|
||||
|
|
@ -10,6 +10,7 @@ import PostHogIdentify from "@/components/PostHogIdentify";
|
|||
import { SentryErrorBoundary } from "@/components/SentryErrorBoundary";
|
||||
import SpinLoader from "@/components/SpinLoader";
|
||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||
import WorkspaceBadge from "@/components/WorkspaceBadge";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { AppConfigProvider } from "@/context/AppConfigContext";
|
||||
import { OnboardingProvider } from "@/context/OnboardingContext";
|
||||
|
|
@ -87,6 +88,7 @@ export default function RootLayout({
|
|||
</AuthProvider>
|
||||
</SentryErrorBoundary>
|
||||
</ThemeProvider>
|
||||
<WorkspaceBadge />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
43
ui/src/components/WorkspaceBadge.tsx
Normal file
43
ui/src/components/WorkspaceBadge.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Dev-only badge that proves which Conductor workspace this UI belongs to.
|
||||
*
|
||||
* Renders nothing unless NEXT_PUBLIC_WORKSPACE_NAME is set — which only happens
|
||||
* when the UI is launched via .conductor/run-ui.sh. So production builds and a
|
||||
* plain `npm run dev` are completely unaffected.
|
||||
*
|
||||
* The pill color is derived deterministically from the workspace name, so two
|
||||
* worktrees running side by side are instantly distinguishable at a glance.
|
||||
*/
|
||||
export default function WorkspaceBadge() {
|
||||
const workspace = process.env.NEXT_PUBLIC_WORKSPACE_NAME;
|
||||
const backend = process.env.NEXT_PUBLIC_BACKEND_URL;
|
||||
const [port, setPort] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setPort(window.location.port);
|
||||
}, []);
|
||||
|
||||
if (!workspace) return null;
|
||||
|
||||
// Deterministic hue from the workspace name.
|
||||
let hash = 0;
|
||||
for (let i = 0; i < workspace.length; i++) {
|
||||
hash = (hash * 31 + workspace.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
const hue = hash % 360;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none fixed bottom-2 left-2 z-[9999] select-none rounded-full px-2.5 py-1 font-mono text-[11px] font-medium text-white shadow-md"
|
||||
style={{ backgroundColor: `hsl(${hue} 70% 38% / 0.92)` }}
|
||||
title={`Conductor workspace: ${workspace}\nUI port: ${port}\nBackend: ${backend ?? "?"}`}
|
||||
>
|
||||
⬡ {workspace}
|
||||
{port ? ` :${port}` : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue