mirror of
https://github.com/penpot/penpot.git
synced 2026-10-08 11:46:17 +00:00
✨ Isolate backend-test database per devenv instance (#12048)
* ✨ Isolate backend-test database per devenv instance Each wsN now gets its own backend-test database (penpot_test_wsN) and Valkey DB (6+N) on the shared infra, so parallel test runs no longer wipe each other. The main database stays shared on purpose. manage.sh creates the test database on bring-up and passes PENPOT_TEST_* into the main container. Test helpers already read those vars, so no runtime code changes. scripts/psql and db-schema gain --ws. AI-assisted-by: muse-spark-1.3-contributor-free * 🐛 Fix per-instance test database creation on bring-up psql -c does not reliably mix SQL with psql-only commands, so the CREATE DATABASE ... WHERE NOT EXISTS ... \gexec one-liner never ran. Use two plain SQL round-trips instead (check pg_database, then CREATE DATABASE) and print what happens, so a future failure shows up in the bring-up output. AI-assisted-by: muse-spark-1.3-contributor-free
This commit is contained in:
parent
60679bbbcd
commit
3f610ad793
@ -1,6 +1,6 @@
|
|||||||
# Devenv startup and configuration
|
# Devenv startup and configuration
|
||||||
|
|
||||||
Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Parallel instances share infra + Postgres + RustFS; each instance has its own `main` container, Valkey, source checkout, tmux session.
|
Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Parallel instances share infra + Postgres + RustFS; each instance has its own `main` container, Valkey, source checkout, tmux session. The main database (`penpot`) and the S3 bucket (`penpot`) are shared by all instances on purpose; backend-test databases are isolated per instance (`penpot_test` on ws0, `penpot_test_wsN` on wsN) with per-instance Valkey DB numbers (`6+N`, since runtime backends occupy `0..5`).
|
||||||
|
|
||||||
## Compose project layout
|
## Compose project layout
|
||||||
|
|
||||||
@ -12,7 +12,8 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
|
|||||||
## Source-of-truth files
|
## Source-of-truth files
|
||||||
|
|
||||||
- `docker/devenv/defaults.env`: ws0 baseline — container/volume names, runtime env, published host ports, tmux defaults. `manage.sh` aborts if unreadable.
|
- `docker/devenv/defaults.env`: ws0 baseline — container/volume names, runtime env, published host ports, tmux defaults. `manage.sh` aborts if unreadable.
|
||||||
- For ws1+, `instance-env-overrides` computes the per-instance overrides (container/volume names, host ports offset `10000·N`, `PENPOT_PUBLIC_URI`, `PENPOT_REDIS_URI`, `PENPOT_BACKEND_WORKER=false`) and `instance-compose` injects them as env vars at compose time — never written to disk, recomputed each call so they can't drift. ws0 uses `defaults.env` as-is.
|
- For ws1+, `instance-env-overrides` computes the per-instance overrides (container/volume names, host ports offset `10000·N`, `PENPOT_PUBLIC_URI`, `PENPOT_REDIS_URI`, `PENPOT_TEST_DATABASE_URI`, `PENPOT_TEST_REDIS_URI`) and `instance-compose` injects them as env vars at compose time — never written to disk, recomputed each call so they can't drift. ws0 uses `defaults.env` as-is.
|
||||||
|
- Per-instance test databases are created by `ensure-instance-test-database` (called from `start-instance`): `CREATE DATABASE` guarded by a `pg_database` existence check, so it is idempotent. Needed because `postgresql_init.sql` only runs on first volume creation. The backend applies migrations itself on first use, so an empty database is enough.
|
||||||
- `backend/scripts/_env`: backend-internal only — secret keys, `PENPOT_FLAGS` (with `enable-backend-worker` gated on `PENPOT_BACKEND_WORKER`), `JAVA_OPTS`, `setup_s3_bucket()`. Never duplicates `defaults.env`.
|
- `backend/scripts/_env`: backend-internal only — secret keys, `PENPOT_FLAGS` (with `enable-backend-worker` gated on `PENPOT_BACKEND_WORKER`), `JAVA_OPTS`, `setup_s3_bucket()`. Never duplicates `defaults.env`.
|
||||||
- Compose files use pure `${VAR}` substitution; missing var = compose fails.
|
- Compose files use pure `${VAR}` substitution; missing var = compose fails.
|
||||||
|
|
||||||
|
|||||||
@ -16,13 +16,20 @@ development.
|
|||||||
# Default connection (penpot db, localhost)
|
# Default connection (penpot db, localhost)
|
||||||
scripts/psql -c "SELECT version();"
|
scripts/psql -c "SELECT version();"
|
||||||
|
|
||||||
# Test database
|
# Test database (ws0)
|
||||||
scripts/psql --test -c "SELECT * FROM migrations;"
|
scripts/psql --test -c "SELECT * FROM migrations;"
|
||||||
|
|
||||||
|
# Isolated test database of another instance (ws1+)
|
||||||
|
scripts/psql --test --ws 1 -c "SELECT * FROM migrations;"
|
||||||
|
|
||||||
# Custom host/user/database
|
# Custom host/user/database
|
||||||
scripts/psql --host myhost --user myuser --db mydb
|
scripts/psql --host myhost --user myuser --db mydb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--ws N` only applies together with `--test` (the main database is
|
||||||
|
shared by all instances): it selects `penpot_test` on `--ws 0` and
|
||||||
|
`penpot_test_wsN` on `--ws N`. `scripts/db-schema` accepts the same flags.
|
||||||
|
|
||||||
`scripts/psql` must be invoked from the repo root so the path resolves.
|
`scripts/psql` must be invoked from the repo root so the path resolves.
|
||||||
|
|
||||||
## Native Tool Available (OpenCode V2)
|
## Native Tool Available (OpenCode V2)
|
||||||
|
|||||||
@ -60,6 +60,13 @@
|
|||||||
(def ^:dynamic *system* nil)
|
(def ^:dynamic *system* nil)
|
||||||
(def ^:dynamic *pool* nil)
|
(def ^:dynamic *pool* nil)
|
||||||
|
|
||||||
|
;; Fallback values used when no PENPOT_TEST_* env vars are set (e.g. CI sets
|
||||||
|
;; them explicitly, see .github/workflows/tests-backend.yml). Inside the
|
||||||
|
;; devenv each wsN container receives per-instance values via
|
||||||
|
;; manage.sh (PENPOT_TEST_DATABASE_URI=postgresql://postgres/penpot_test_wsN,
|
||||||
|
;; PENPOT_TEST_REDIS_URI=redis://valkey/<6+N>), so parallel test runs on
|
||||||
|
;; different workspaces never share a database. cf/read-config picks the env
|
||||||
|
;; vars up through the "penpot-test" prefix.
|
||||||
(def default
|
(def default
|
||||||
{:database-uri "postgresql://postgres/penpot_test"
|
{:database-uri "postgresql://postgres/penpot_test"
|
||||||
:redis-uri "redis://valkey/1"
|
:redis-uri "redis://valkey/1"
|
||||||
|
|||||||
@ -81,6 +81,11 @@ services:
|
|||||||
- PENPOT_DATABASE_PASSWORD=${PENPOT_DATABASE_PASSWORD}
|
- PENPOT_DATABASE_PASSWORD=${PENPOT_DATABASE_PASSWORD}
|
||||||
- PENPOT_DATABASE_MAX_POOL_SIZE=${PENPOT_DATABASE_MAX_POOL_SIZE}
|
- PENPOT_DATABASE_MAX_POOL_SIZE=${PENPOT_DATABASE_MAX_POOL_SIZE}
|
||||||
- PENPOT_REDIS_URI=${PENPOT_REDIS_URI}
|
- PENPOT_REDIS_URI=${PENPOT_REDIS_URI}
|
||||||
|
# Per-instance backend-test config. Values come from
|
||||||
|
# instance-env-overrides in manage.sh; the main database above stays
|
||||||
|
# shared by all instances on purpose.
|
||||||
|
- PENPOT_TEST_DATABASE_URI=${PENPOT_TEST_DATABASE_URI}
|
||||||
|
- PENPOT_TEST_REDIS_URI=${PENPOT_TEST_REDIS_URI}
|
||||||
- PENPOT_OBJECTS_STORAGE_BACKEND=${PENPOT_OBJECTS_STORAGE_BACKEND}
|
- PENPOT_OBJECTS_STORAGE_BACKEND=${PENPOT_OBJECTS_STORAGE_BACKEND}
|
||||||
- PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}
|
- PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}
|
||||||
- PENPOT_OBJECTS_STORAGE_S3_BUCKET=${PENPOT_OBJECTS_STORAGE_S3_BUCKET}
|
- PENPOT_OBJECTS_STORAGE_S3_BUCKET=${PENPOT_OBJECTS_STORAGE_S3_BUCKET}
|
||||||
|
|||||||
73
manage.sh
73
manage.sh
@ -94,6 +94,9 @@ set -e
|
|||||||
# instance-compose wrap 'docker compose' for one instance's main
|
# instance-compose wrap 'docker compose' for one instance's main
|
||||||
# project, injecting that instance's overrides
|
# project, injecting that instance's overrides
|
||||||
# instance-env-overrides the per-instance KEY=VALUE overrides
|
# instance-env-overrides the per-instance KEY=VALUE overrides
|
||||||
|
# instance-test-db-name test database name for one instance
|
||||||
|
# instance-test-redis-db Valkey DB number for one instance's tests
|
||||||
|
# ensure-instance-test-database create one instance's test database
|
||||||
# devenv-main-container resolve the 'main' container id via compose ps
|
# devenv-main-container resolve the 'main' container id via compose ps
|
||||||
# devenv-main-running true if 'main' is up
|
# devenv-main-running true if 'main' is up
|
||||||
#
|
#
|
||||||
@ -301,6 +304,43 @@ function ensure-infra-up {
|
|||||||
infra-compose up -d --wait --wait-timeout 60 --remove-orphans
|
infra-compose up -d --wait --wait-timeout 60 --remove-orphans
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Create the backend-test database for <instance> on the shared Postgres
|
||||||
|
# if it does not exist yet. Idempotent: safe to run on every bring-up.
|
||||||
|
# Needed because postgresql_init.sql only runs when the Postgres volume is
|
||||||
|
# created for the first time, so per-instance test databases (which did not
|
||||||
|
# exist back then) would otherwise never be created on existing volumes.
|
||||||
|
# The backend applies migrations itself on first use, so an empty database
|
||||||
|
# is enough here. (Two plain SQL round-trips on purpose: psql -c does not
|
||||||
|
# reliably mix SQL with psql-only commands like \gexec.)
|
||||||
|
function ensure-instance-test-database {
|
||||||
|
local instance="$1"
|
||||||
|
local dbname
|
||||||
|
dbname=$(instance-test-db-name "$instance")
|
||||||
|
|
||||||
|
local pg_container
|
||||||
|
pg_container=$(infra-compose ps -q postgres 2>/dev/null)
|
||||||
|
if [[ -z "$pg_container" ]]; then
|
||||||
|
echo "[${instance}] postgres container not found; is shared infra up?" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a psql_base=(docker exec
|
||||||
|
-e "PGPASSWORD=${PENPOT_DATABASE_PASSWORD:-penpot}"
|
||||||
|
"$pg_container"
|
||||||
|
psql -h 127.0.0.1 -U "${PENPOT_DATABASE_USERNAME:-penpot}"
|
||||||
|
-d postgres -v ON_ERROR_STOP=1 -tA)
|
||||||
|
|
||||||
|
local exists
|
||||||
|
exists=$("${psql_base[@]}" -c "SELECT 1 FROM pg_database WHERE datname = '${dbname}'")
|
||||||
|
if [[ "$exists" == "1" ]]; then
|
||||||
|
echo "[${instance}] test database ${dbname} already exists."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[${instance}] creating test database ${dbname} ..."
|
||||||
|
"${psql_base[@]}" -c "CREATE DATABASE \"${dbname}\""
|
||||||
|
}
|
||||||
|
|
||||||
# Refuse to sync workspaces if the live repo is in a fragile Git state.
|
# Refuse to sync workspaces if the live repo is in a fragile Git state.
|
||||||
# Copying a partial rebase/merge/cherry-pick into all workspaces would leave
|
# Copying a partial rebase/merge/cherry-pick into all workspaces would leave
|
||||||
# every instance in the same broken state.
|
# every instance in the same broken state.
|
||||||
@ -333,6 +373,33 @@ function instance-port {
|
|||||||
echo $(( base + n * PENPOT_INSTANCE_PORT_STRIDE ))
|
echo $(( base + n * PENPOT_INSTANCE_PORT_STRIDE ))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Echo the backend-test database name for <instance>. ws0 keeps the
|
||||||
|
# historical name so existing flows stay untouched; ws1+ get an isolated
|
||||||
|
# database each on the shared Postgres server. The main database stays
|
||||||
|
# shared by all instances on purpose.
|
||||||
|
function instance-test-db-name {
|
||||||
|
local instance="$1"
|
||||||
|
local n=0
|
||||||
|
[[ "$instance" =~ ^ws([0-9]+)$ ]] && n="${BASH_REMATCH[1]}"
|
||||||
|
if (( n == 0 )); then
|
||||||
|
echo "penpot_test"
|
||||||
|
else
|
||||||
|
echo "penpot_test_ws${n}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Echo the Valkey DB number reserved for the backend tests of <instance>.
|
||||||
|
# Runtime backends already occupy 0..PENPOT_MAX_WS_INDEX on the shared
|
||||||
|
# Valkey, so test DBs start right after to never share a DB with a
|
||||||
|
# running backend. (Resolved at call time; PENPOT_MAX_WS_INDEX is defined
|
||||||
|
# further below but always set before any call.)
|
||||||
|
function instance-test-redis-db {
|
||||||
|
local instance="$1"
|
||||||
|
local n=0
|
||||||
|
[[ "$instance" =~ ^ws([0-9]+)$ ]] && n="${BASH_REMATCH[1]}"
|
||||||
|
echo $(( PENPOT_MAX_WS_INDEX + 1 + n ))
|
||||||
|
}
|
||||||
|
|
||||||
# Echo the per-instance Compose variable overrides for a workspace, one
|
# Echo the per-instance Compose variable overrides for a workspace, one
|
||||||
# KEY=VALUE per line, for instance-compose to inject into its `env -i` line.
|
# KEY=VALUE per line, for instance-compose to inject into its `env -i` line.
|
||||||
# Compose gives shell-env precedence over --env-file, so these override the
|
# Compose gives shell-env precedence over --env-file, so these override the
|
||||||
@ -358,11 +425,15 @@ function instance-env-overrides {
|
|||||||
opencode=$(instance-port "$instance" "$PENPOT_PORT_BASE_OPENCODE")
|
opencode=$(instance-port "$instance" "$PENPOT_PORT_BASE_OPENCODE")
|
||||||
mdts=$(instance-port "$instance" "$PENPOT_PORT_BASE_MDTS")
|
mdts=$(instance-port "$instance" "$PENPOT_PORT_BASE_MDTS")
|
||||||
storybook=$(instance-port "$instance" "$PENPOT_PORT_BASE_STORYBOOK")
|
storybook=$(instance-port "$instance" "$PENPOT_PORT_BASE_STORYBOOK")
|
||||||
|
test_db=$(instance-test-db-name "$instance")
|
||||||
|
test_redis_db=$(instance-test-redis-db "$instance")
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
"PENPOT_MAIN_CONTAINER_NAME=penpot-devenv-${instance}-main" \
|
"PENPOT_MAIN_CONTAINER_NAME=penpot-devenv-${instance}-main" \
|
||||||
"PENPOT_USER_DATA_VOLUME=penpotdev_${instance}_user_data" \
|
"PENPOT_USER_DATA_VOLUME=penpotdev_${instance}_user_data" \
|
||||||
"PENPOT_PUBLIC_URI=https://localhost:${public_https}" \
|
"PENPOT_PUBLIC_URI=https://localhost:${public_https}" \
|
||||||
"PENPOT_REDIS_URI=redis://valkey/${n}" \
|
"PENPOT_REDIS_URI=redis://valkey/${n}" \
|
||||||
|
"PENPOT_TEST_DATABASE_URI=postgresql://postgres/${test_db}" \
|
||||||
|
"PENPOT_TEST_REDIS_URI=redis://valkey/${test_redis_db}" \
|
||||||
"PENPOT_PUBLIC_HTTPS_PORT=${public_https}" \
|
"PENPOT_PUBLIC_HTTPS_PORT=${public_https}" \
|
||||||
"PENPOT_PUBLIC_HTTP_PORT=${public}" \
|
"PENPOT_PUBLIC_HTTP_PORT=${public}" \
|
||||||
"PENPOT_MCP_SERVER_PORT=${mcp}" \
|
"PENPOT_MCP_SERVER_PORT=${mcp}" \
|
||||||
@ -709,6 +780,8 @@ function start-instance {
|
|||||||
local git_user_email="${4:-}"
|
local git_user_email="${4:-}"
|
||||||
local agentic="${5:-true}"
|
local agentic="${5:-true}"
|
||||||
|
|
||||||
|
ensure-instance-test-database "$instance"
|
||||||
|
|
||||||
instance-compose "$instance" up -d main
|
instance-compose "$instance" up -d main
|
||||||
|
|
||||||
# Wait briefly for main to be reachable; the tmux session lives inside.
|
# Wait briefly for main to be reachable; the tmux session lives inside.
|
||||||
|
|||||||
@ -5,11 +5,22 @@ HOST="${PENPOT_DB_HOST:-postgres}"
|
|||||||
USER="${PENPOT_DB_USER:-penpot}"
|
USER="${PENPOT_DB_USER:-penpot}"
|
||||||
PASSWORD="${PENPOT_DB_PASSWORD:-penpot}"
|
PASSWORD="${PENPOT_DB_PASSWORD:-penpot}"
|
||||||
DB="${PENPOT_DB_NAME:-penpot}"
|
DB="${PENPOT_DB_NAME:-penpot}"
|
||||||
|
TEST=false
|
||||||
|
WS=""
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--test)
|
--test)
|
||||||
DB="penpot_test"
|
DB="penpot_test"
|
||||||
|
TEST=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--ws)
|
||||||
|
WS="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ws*)
|
||||||
|
WS="${1#--ws}"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--host|-h)
|
--host|-h)
|
||||||
@ -30,5 +41,25 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# --ws selects one instance's isolated backend-test database
|
||||||
|
# (penpot_test on ws0, penpot_test_wsN on wsN). It only applies together
|
||||||
|
# with --test because the main database is shared by all instances.
|
||||||
|
if [[ -n "$WS" ]]; then
|
||||||
|
if [[ "$WS" =~ ^ws([0-9]+)$ ]]; then
|
||||||
|
WS="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
if [[ ! "$WS" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Invalid --ws value: '$WS' (expected a non-negative integer, e.g. --ws 1)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$TEST" != "true" ]]; then
|
||||||
|
echo "--ws only applies together with --test (the main database is shared by all instances)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$WS" != "0" ]]; then
|
||||||
|
DB="penpot_test_ws${WS}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
export PGPASSWORD="$PASSWORD"
|
export PGPASSWORD="$PASSWORD"
|
||||||
exec pg_dump -h "$HOST" -U "$USER" -d "$DB" --schema-only --no-owner --no-privileges "$@"
|
exec pg_dump -h "$HOST" -U "$USER" -d "$DB" --schema-only --no-owner --no-privileges "$@"
|
||||||
|
|||||||
31
scripts/psql
31
scripts/psql
@ -5,11 +5,22 @@ HOST="${PENPOT_DB_HOST:-postgres}"
|
|||||||
USER="${PENPOT_DB_USER:-penpot}"
|
USER="${PENPOT_DB_USER:-penpot}"
|
||||||
PASSWORD="${PENPOT_DB_PASSWORD:-penpot}"
|
PASSWORD="${PENPOT_DB_PASSWORD:-penpot}"
|
||||||
DB="${PENPOT_DB_NAME:-penpot}"
|
DB="${PENPOT_DB_NAME:-penpot}"
|
||||||
|
TEST=false
|
||||||
|
WS=""
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--test)
|
--test)
|
||||||
DB="penpot_test"
|
DB="penpot_test"
|
||||||
|
TEST=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--ws)
|
||||||
|
WS="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ws*)
|
||||||
|
WS="${1#--ws}"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--host|-h)
|
--host|-h)
|
||||||
@ -30,5 +41,25 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# --ws selects one instance's isolated backend-test database
|
||||||
|
# (penpot_test on ws0, penpot_test_wsN on wsN). It only applies together
|
||||||
|
# with --test because the main database is shared by all instances.
|
||||||
|
if [[ -n "$WS" ]]; then
|
||||||
|
if [[ "$WS" =~ ^ws([0-9]+)$ ]]; then
|
||||||
|
WS="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
if [[ ! "$WS" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Invalid --ws value: '$WS' (expected a non-negative integer, e.g. --ws 1)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$TEST" != "true" ]]; then
|
||||||
|
echo "--ws only applies together with --test (the main database is shared by all instances)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$WS" != "0" ]]; then
|
||||||
|
DB="penpot_test_ws${WS}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
export PGPASSWORD="$PASSWORD"
|
export PGPASSWORD="$PASSWORD"
|
||||||
exec psql -h "$HOST" -U "$USER" -d "$DB" "$@"
|
exec psql -h "$HOST" -U "$USER" -d "$DB" "$@"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user