Qspinuplocal
Ticket → production-PR pipeline for Claude Code & Codex CLI, enforced by hooks. 21-skill pipeline, one configurator, adapts to any codebase.
npx -y skills add 3awny/qship --skill qspinuplocalAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Spin up your primary service locally against a local Postgres DB for testing — env wiring, optional migrations, health check. Single-service by design; adapt the start command to your stack.
SKILL.md
11.7 KB, as published. Nobody here has run it
qspinuplocal — Spin Up the Local Stack
Starts your primary repo's service against a local Postgres database so /qe2etest and /qmanualt have a real (not mocked) target to drive.
Scope — read this first. Local-stack spin-up is the single most stack-specific thing in qship: every codebase starts differently (uvicorn, gunicorn,
manage.py runserver,npm run dev,docker compose up, a Procfile, a Makefile target). This skill therefore does the portable parts well — DB existence check, port-collision handling, theload_dotenv(override=True)footgun, worktree.envsafety, a sharedports.envcontract for downstream skills — and starts one service (your primary repo, resolved from$SKILLS_ROOT/qship/repos.json). If you run multiple services locally (a worker, a sibling API, a separate frontend), start those the same way or adapt the Step 5 start command. Treat the bash below as a reference recipe, not a turnkey generic orchestrator.
Input: $ARGUMENTS
--db-name <db>— local Postgres DB to start against (default:{{LOCAL_DEV_DB_NAME}}). Must already exist — clone via/qlocalclonedbfirst if your project supports it.--tenant-uuid <uuid>— optionalDEV_TENANT_IDto inject (multi-tenant apps); if omitted, looked up from<db>.app.account_registrywhen that table exists.--worktree <path>— the checkout to run against. Defaults to an auto-detected worktree under{{STATE_ROOT}}/worktrees/<TICKET>/, else refuses the maintree unless--allow-maintree.--start-cmd "<cmd>"— override the service start command (default tries the project's configured runner).--port <n>— preferred port (default8000, with a collision-fallback ladder).--worker— also start a background worker process if your stack has one (best-effort).
Resolve the target repo
REPO_NAME="$(jq -r '(.[] | select(.is_primary==true) | .name) // .[0].name' "$SKILLS_ROOT/qship/repos.json")"
REPO_SCHEMA="$(jq -r '(.[] | select(.is_primary==true) | .schema) // .[0].schema // "public"' "$SKILLS_ROOT/qship/repos.json")"
REPO_PORT="$(jq -r '(.[] | select(.is_primary==true) | .port) // .[0].port // 8000' "$SKILLS_ROOT/qship/repos.json")"
echo "Primary repo: $REPO_NAME (schema=$REPO_SCHEMA, default port=$REPO_PORT)"
Protocol
Step 0 — Resolve --db-name, --tenant-uuid, and the worktree path
DB_NAME="${DB_NAME:-{{LOCAL_DEV_DB_NAME}}}"
# Verify the DB exists locally
psql -U {{LOCAL_DB_USER}} -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1 \
|| { echo "❌ DB '$DB_NAME' does not exist. Create it (or run /qlocalclonedb) first."; exit 1; }
# Multi-tenant apps only: resolve a tenant UUID from the DB if not supplied.
if [ -z "${TENANT_UUID:-}" ]; then
TENANT_UUID="$(psql -U {{LOCAL_DB_USER}} -d "$DB_NAME" -tAc \
'SELECT tenant_id FROM app.account_registry LIMIT 1' 2>/dev/null || true)"
fi
echo "Using DB=$DB_NAME${TENANT_UUID:+ tenant_uuid=$TENANT_UUID}"
Step 0.5 — Resolve the worktree (worktree-isolation contract)
This skill MUTATES the repo's .env (Step 4.5). It MUST NEVER touch the maintree .env, because you run day-job work against the maintree and the backup/restore dance is unsafe across overlapping sessions.
MAINTREE="{{CODEBASE_ROOT}}/$REPO_NAME"
REPO_DIR="${WORKTREE:-}"
# Auto-detect: if invoked from inside {{STATE_ROOT}}/worktrees/<TICKET>/, infer
# the repo's worktree from siblings unless explicitly overridden.
if [ -z "$REPO_DIR" ]; then
PWD_REAL="$(pwd -P)"
case "$PWD_REAL" in
{{STATE_ROOT}}/worktrees/*|/private{{STATE_ROOT}}/worktrees/*)
TICKET_DIR="$(echo "$PWD_REAL" | sed -E 's|^(/private)?({{STATE_ROOT}}/worktrees/[^/]+).*|\2|')"
[ -d "$TICKET_DIR/$REPO_NAME" ] && REPO_DIR="$TICKET_DIR/$REPO_NAME"
;;
esac
fi
# Allow explicit maintree fallback ONLY when the caller opts in.
if [ -z "$REPO_DIR" ]; then
if [ "${ALLOW_MAINTREE:-0}" = "1" ]; then REPO_DIR="$MAINTREE"
else echo "❌ refusing to use the maintree (Step 4.5 would rewrite its .env). Pass --worktree <path> or set ALLOW_MAINTREE=1."; exit 1; fi
fi
if [ "${ALLOW_MAINTREE:-0}" != "1" ] && [ "$(cd "$REPO_DIR" && pwd -P)" = "$MAINTREE" ]; then
echo "❌ Step 4.5 .env rewrite would land in the maintree at $REPO_DIR. Aborting."
exit 1
fi
echo "Using repo dir: $REPO_DIR (maintree=$([ "${ALLOW_MAINTREE:-0}" = 1 ] && echo allowed || echo forbidden))"
Hard rules:
- Every later step uses
$REPO_DIR, never the maintree path. - Venvs /
node_modulesare sourced from wherever the repo provides them; only the.envmutation is worktree-scoped. - Maintree use requires explicit
--allow-maintree(you handle the.envrestore yourself).
Step 1 — Preflight + pick a port
# DB server reachable?
psql -U {{LOCAL_DB_USER}} -d postgres -c "SELECT 1" > /dev/null 2>&1 \
|| { echo "❌ Postgres not reachable. Start your local Postgres and retry."; exit 1; }
# Port detection + deterministic fallback (no mid-run prompting).
pick_port() {
local candidate
for candidate in "$@"; do
lsof -tiTCP:"$candidate" -sTCP:LISTEN >/dev/null 2>&1 || { echo "$candidate"; return 0; }
echo "⚠ port $candidate busy — trying next" >&2
done
echo "❌ no free port in candidate list" >&2; return 1
}
SERVICE_PORT=$(pick_port "${REPO_PORT:-8000}" 18000 18001 18002) || exit 1
SERVICE_URL="http://127.0.0.1:${SERVICE_PORT}"
# Shared contract so /qe2etest, /qmanualt, curl smoke checks all hit the same port.
cat > /tmp/{{COMPANY_SLUG}}-ports.env <<EOF
# Written by /qspinuplocal — source via: set -a; . /tmp/{{COMPANY_SLUG}}-ports.env; set +a
export {{ENV_SERVICE_URL_KEY}}=${SERVICE_URL}
export SERVICE_PORT=${SERVICE_PORT}
export SERVICE_HEALTH=${SERVICE_URL}/health
EOF
echo "→ ${REPO_NAME} on ${SERVICE_URL} (saved to /tmp/{{COMPANY_SLUG}}-ports.env)"
Step 2 — (optional) Re-clone / reset the local DB
Only if your project ships a local-DB bootstrap (e.g. /qlocalclonedb). Skip otherwise. Don't wipe local data the user may care about — ask first.
Step 3 — Verify the DB is usable
PSQL="psql -U {{LOCAL_DB_USER}} -d $DB_NAME"
$PSQL -c "SELECT 1" >/dev/null 2>&1 || { echo "❌ cannot connect to $DB_NAME"; exit 1; }
# Multi-tenant apps: sanity-check the registry has rows.
$PSQL -tAc "SELECT COUNT(*) FROM app.account_registry" 2>/dev/null || true
Step 4 — Apply pending migrations (only if this repo has migrations)
HAS_MIGRATIONS="$(jq -r '(.[] | select(.is_primary==true) | .has_migrations) // false' "$SKILLS_ROOT/qship/repos.json")"
if [ "$HAS_MIGRATIONS" = "true" ]; then
cd "$REPO_DIR"
LOCAL_DB="postgresql://{{LOCAL_DB_USER}}@localhost:5432/$DB_NAME"
# Adapt to your migration tool (alembic shown):
DATABASE_URL="$LOCAL_DB" ./venv/bin/alembic upgrade head || echo "⚠ migration step failed — inspect before starting"
fi
Step 4.5 — Point the repo's .env at the local DB (MANDATORY for Python apps using load_dotenv(override=True))
The footgun this step exists for: if your service calls
load_dotenv(override=True)at import time, it CLOBBERS anyDATABASE_URLyou export on the command line with whatever is written in.env. Exporting env vars tonohup uvicorn ...is then NOT enough — the running process silently uses the stale.envDB. The only reliable fix is to rewrite.envitself (with a backup). If your stack reads env vars at request time (override=False) you can skip this, but rewriting is the safe default.
cd "$REPO_DIR"
LOCAL_DB="postgresql://{{LOCAL_DB_USER}}@localhost:5432/$DB_NAME"
PWD_NOW="$(pwd -P)"
if [ "${ALLOW_MAINTREE:-0}" != "1" ] && [ "$PWD_NOW" = "$MAINTREE" ]; then
echo "❌ refusing to rewrite maintree .env at $PWD_NOW"; exit 1
fi
SPINUP_TS=$(date +%s)
BACKUP=".env.backup-spinup-$SPINUP_TS"
[ -f .env ] && cp .env "$BACKUP" && echo "Backed up .env -> $PWD_NOW/$BACKUP"
# Rewrite the DB-pointing keys that exist; add the critical ones if missing.
# Preserves all other keys (API tokens, auth provider config, etc.).
for key in DATABASE_URL GLOBAL_DATABASE_URL ENFORCE_DEV_DATABASE_URL; do
if grep -q "^${key}=" .env 2>/dev/null; then
sed -i.tmp "s|^${key}=.*|${key}=$LOCAL_DB|" .env
fi
done
# ENFORCE_DEV_DATABASE_URL short-circuits per-tenant DB routing in multi-tenant
# apps — add it even if absent so the session lands in the local DB.
grep -q '^ENFORCE_DEV_DATABASE_URL=' .env 2>/dev/null || echo "ENFORCE_DEV_DATABASE_URL=$LOCAL_DB" >> .env
[ -n "${TENANT_UUID:-}" ] && { grep -q '^DEV_TENANT_ID=' .env && sed -i.tmp "s|^DEV_TENANT_ID=.*|DEV_TENANT_ID=$TENANT_UUID|" .env || echo "DEV_TENANT_ID=$TENANT_UUID" >> .env; }
# Make any in-repo base-URL point at the port we actually bound.
if grep -q "^{{ENV_SERVICE_URL_KEY}}=" .env 2>/dev/null; then
sed -i.tmp "s|^{{ENV_SERVICE_URL_KEY}}=.*|{{ENV_SERVICE_URL_KEY}}=$SERVICE_URL|" .env
fi
rm -f .env.tmp
echo "Rewrote .env DB pointers -> $DB_NAME"
Restore when done: cp "$BACKUP" .env in $REPO_DIR. Surface the backup path in the final report.
Step 5 — Start the service
Use the project's start command. Detect a sensible default, or take --start-cmd:
cd "$REPO_DIR"
START_CMD="${START_CMD:-}"
if [ -z "$START_CMD" ]; then
# Best-effort defaults — adapt to your stack.
if [ -f manage.py ]; then START_CMD="./venv/bin/python manage.py runserver 127.0.0.1:$SERVICE_PORT"
elif [ -f package.json ] && grep -q '"dev"' package.json; then START_CMD="npm run dev -- --port $SERVICE_PORT"
elif ls ./venv/bin/uvicorn >/dev/null 2>&1; then START_CMD="./venv/bin/uvicorn app.main:app --host 127.0.0.1 --port $SERVICE_PORT"
else echo "❌ could not detect a start command — pass --start-cmd \"<cmd>\""; exit 1; fi
fi
# Detach safely so the tool call returns (close all stdio; see qship Bash-tool hang rules).
DATABASE_URL="postgresql://{{LOCAL_DB_USER}}@localhost:5432/$DB_NAME" \
nohup $START_CMD > /tmp/{{COMPANY_SLUG}}-service.log 2>&1 </dev/null &
echo "Started: $START_CMD (pid $!, log /tmp/{{COMPANY_SLUG}}-service.log)"
If your stack has a worker and --worker was passed, start it the same way against the same DATABASE_URL.
Step 6 — Health check
for i in $(seq 1 20); do
if curl -fsS "$SERVICE_URL/health" >/dev/null 2>&1; then echo "✅ $REPO_NAME healthy on $SERVICE_URL"; break; fi
sleep 1
[ "$i" = 20 ] && { echo "❌ service did not become healthy — tail /tmp/{{COMPANY_SLUG}}-service.log"; tail -30 /tmp/{{COMPANY_SLUG}}-service.log; exit 1; }
done
Step 7 — Report
Print: the repo + worktree used, the bound port / URL, the DB name, the .env backup path, and the log path. If /qe2etest or /qmanualt will run next, remind the caller to source /tmp/{{COMPANY_SLUG}}-ports.env so they hit the same port.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Service uses the wrong DB despite exported env | load_dotenv(override=True) clobbered your export at import | Step 4.5 rewrites .env — confirm it ran against $REPO_DIR, not the maintree |
Connection refused on a base-URL the UI calls | stale URL in .env | Step 4.5 rewrites {{ENV_SERVICE_URL_KEY}} to the bound port |
| Port already in use | another stack / IDE / Docker holds it | the fallback ladder picks 18000+; the real port is in /tmp/{{COMPANY_SLUG}}-ports.env |
| Multi-tenant 403 "no access to organization" | session landed in the wrong DB | ensure ENFORCE_DEV_DATABASE_URL and DEV_TENANT_ID were written in Step 4.5 |
Cleanup
lsof -ti:"${SERVICE_PORT:-8000}" | xargs -r kill -9
# restore the repo's .env from the Step 4.5 backup