NEWCloud agents are live.See how

Run tickets in parallel without collisions

A worktree gives each ticket its own files, but not its own database or port. A short setup script you commit to the repository gives it both.

Last updated View as Markdown

The problem

A worktree isolates code. It does not isolate anything outside its folder. Every ticket's checkout still reads the same .env values and config/database.yml defaults, so every ticket still points at the same development database, the same test database and the same port.

Run three tickets at once and they collide:

  • Ports. Two dev servers both try port 3000, and the second one dies with EADDRINUSE.
  • Schema. One ticket's migration changes the shared database under another ticket's tests.
  • Test data. Two test suites clean and reseed one test database at the same moment, and runs fail at random or deadlock.

The fix is to give every worktree its own database names and its own port. You write that once, as a script, and commit it.

The fix: .fredrin/setup.sh

Commit a shell script at .fredrin/setup.sh. When Fredrin creates a fresh worktree for a ticket, it runs the script there before the Worker starts, so the Worker lands in a checkout that already has its own databases.

Where it runsOn a remote agent: any machine running fredrin-agent. Not for Workers the desktop app starts on its own machine (see below).
WhenOnce, right after the ticket's worktree is created. A reused worktree (a resume, or another run of the same ticket) skips it, except one whose node_modules has since been removed, which runs it again when the script offers --dependencies.
Howsh .fredrin/setup.sh from the worktree root. The file does not need its executable bit.
Arguments--dependencies if that word appears anywhere in the script's text, otherwise none. A script that never mentions it is never handed a flag it does not understand.
EnvironmentThe machine's own environment, plus FREDRIN_HOOK=setup and a PATH widened to the usual CLI directories. No Fredrin credentials: no API token and no API address.
Time limit10 minutes, or 15 when run with --dependencies. Then the script and every process it started are killed.
On failureAdvisory. A non-zero exit or a timeout is written to the job log as a [setup] line, and the Worker starts anyway, so it can read the line and repair.

After the script, if the worktree still has no .env but the project commits a .env.example, Fredrin copies the example to .env. It never overwrites a .env the script wrote. To turn the script off on one machine, set FREDRIN_AGENT_NO_SETUP=1 in that agent's environment.

On the desktop app's own machine

Today the desktop app does not run .fredrin/setup.sh for Workers it starts on its own machine. Those worktrees keep sharing whatever your configuration points at until something prepares them. Two ways to close that gap:

  • Run it yourself in the worktree: sh .fredrin/setup.sh.
  • Tell the Worker to run it. The instructions snippet below makes a missing .env.local the first thing a Worker fixes.

One part is already handled: when the desktop app starts a ticket's dev server for you, it picks a free port and exports it as PORT.

Rails and Postgres

This script names everything after the worktree folder, plus a checksum of its full path so two folders with the same name never share a database. It writes the names to .env.local, then creates and loads both databases.

Terminal
#!/bin/sh
# .fredrin/setup.sh: give this ticket worktree its own databases and port.
set -eu

app=myapp                       # prefix for this app's database names
pg=postgres://localhost:5432    # your Postgres server; add user:password@ if it needs them

# A name unique to this worktree: the folder name, plus a checksum of the
# full path so two folders with the same name never share a database.
folder=$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9\n' '_' | cut -c1-30)
sum=$(printf '%s' "$PWD" | cksum | cut -d ' ' -f 1)
slug="${folder}_$((sum % 100000))"
port=$((3100 + sum % 800))      # stable per worktree, 3100 to 3899, never 3000

dev_url="$pg/${app}_dev_$slug"
test_url="$pg/${app}_test_$slug"

cat > .env.local <<ENV
DATABASE_URL=$dev_url
TEST_DATABASE_URL=$test_url
PORT=$port
ENV
# dotenv-rails does not load .env.local in the test environment, so tests read this one.
printf 'TEST_DATABASE_URL=%s\n' "$test_url" > .env.test.local

bundle check >/dev/null 2>&1 || bundle install
DATABASE_URL="$dev_url" bin/rails db:prepare
RAILS_ENV=test TEST_DATABASE_URL="$test_url" bin/rails db:prepare

db:prepare creates a database that does not exist yet and loads the schema, so the Postgres role needs permission to create databases. The derivation uses only POSIX shell, tr, cut and cksum, so it gives the same answer under dash (/bin/sh on Debian and Ubuntu) and bash. Two worktrees can occasionally land on the same port; edit PORT in that worktree's .env.local if they do.

Node and Prisma

Same derivation, then Prisma's migrations against both databases. Prisma's CLI does not read .env.local, so the script passes each URL inline.

Terminal
#!/bin/sh
set -eu
app=myapp
pg=postgres://localhost:5432
folder=$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9\n' '_' | cut -c1-30)
sum=$(printf '%s' "$PWD" | cksum | cut -d ' ' -f 1)
slug="${folder}_$((sum % 100000))"
dev_url="$pg/${app}_dev_$slug"
test_url="$pg/${app}_test_$slug"

printf 'DATABASE_URL=%s\nTEST_DATABASE_URL=%s\nPORT=%s\n' \
  "$dev_url" "$test_url" "$((3100 + sum % 800))" > .env.local

[ -d node_modules ] || npm ci
DATABASE_URL="$dev_url" npx prisma migrate deploy
DATABASE_URL="$test_url" npx prisma migrate deploy

prisma migrate deploy creates the database if it does not exist. Next.js reads .env.local on its own; for plain Node use node --env-file=.env.local, and have your test setup use TEST_DATABASE_URL.

Django

Django's test runner already names its test database after the main one (test_ plus the name), so a per-worktree main database gives you a per-worktree test database for free. Django does not create the main one, so the script does.

Terminal
#!/bin/sh
set -eu
app=myapp
pg=postgres://localhost:5432
folder=$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9\n' '_' | cut -c1-30)
sum=$(printf '%s' "$PWD" | cksum | cut -d ' ' -f 1)
db="${app}_dev_${folder}_$((sum % 100000))"

printf 'DATABASE_URL=%s\nPORT=%s\n' "$pg/$db" "$((3100 + sum % 800))" > .env.local

psql "$pg/postgres" -tAc "SELECT 1 FROM pg_database WHERE datname = '$db'" | grep -q 1 \
  || psql "$pg/postgres" -c "CREATE DATABASE $db"
DATABASE_URL="$pg/$db" python manage.py migrate

Point the app at it

The script only writes names. The app has to read them.

Rails. Read the URLs in config/database.yml, and add dotenv-rails to the :development and :test groups of your Gemfile so .env.local and .env.test.local are loaded:

YAML
development:
  <<: *default
  database: myapp_development
  url: <%= ENV["DATABASE_URL"] %>

test:
  <<: *default
  database: myapp_test
  url: <%= ENV["TEST_DATABASE_URL"] %>

Rails merges a DATABASE_URL it finds in the process environment into whichever environment is running, tests included. So never export DATABASE_URL in a shell you run tests from; let dotenv load it. That is why the test database's name lives in its own .env.test.local.

Node and Prisma. Nothing to change if schema.prisma (or prisma.config.ts) already reads env("DATABASE_URL").

Django. Build DATABASES from the URL, for example with dj-database-url, after loading .env.local with python-dotenv:

python
from dotenv import load_dotenv
import dj_database_url

load_dotenv(BASE_DIR / ".env.local")
DATABASES = {"default": dj_database_url.config(default="postgres://localhost:5432/myapp")}

Ports. Most servers decide their port before they read any .env file, so pass it on the command line, read straight from the file:

Terminal
bin/rails server -p "$(sed -n 's/^PORT=//p' .env.local)"
npx next dev -p "$(sed -n 's/^PORT=//p' .env.local)"
python manage.py runserver "$(sed -n 's/^PORT=//p' .env.local)"

Git. Keep the generated files out of commits:

Text
# .gitignore
.env.local
.env.test.local

Tell your Workers

A Worker reads your repository's CLAUDE.md or AGENTS.md before it starts. A few lines there are the lighter alternative to the script, and the only automatic route on the desktop app's own machine. They are best-effort: they work when the Worker follows them, which is not a guarantee the script gives you.

Markdown
## Parallel worktrees

Every ticket worktree has its own databases and its own port, written to
`.env.local` by `.fredrin/setup.sh`.

- If `.env.local` is missing, run `sh .fredrin/setup.sh` from the worktree
  root before anything that touches the database or starts a server.
- Start servers on the port in `.env.local`:
  `bin/rails server -p "$(sed -n 's/^PORT=//p' .env.local)"`.
- Never fall back to port 3000 or to the shared `myapp_development` and
  `myapp_test` databases. If the worktree's own database is unreachable,
  stop and say so instead.

Cleanup

Be aware that nothing drops these databases for you. When a ticket's worktree is removed, its databases stay on your Postgres server until you remove them.

List what has accumulated:

Terminal
psql postgres://localhost:5432/postgres -tAc "SELECT datname FROM pg_database WHERE datname ~ '^(test_)?myapp_(dev|test)_'"

Drop them all:

Terminal
psql postgres://localhost:5432/postgres -tAc "SELECT datname FROM pg_database WHERE datname ~ '^(test_)?myapp_(dev|test)_'" | while read -r db; do psql postgres://localhost:5432/postgres -c "DROP DATABASE \"$db\""; done

That also drops the databases of worktrees still in use. In any you keep, run sh .fredrin/setup.sh again to recreate them.

Next steps