[settings] # Put this repo's pinned tools AHEAD of anything already on PATH. Without it a Homebrew `node`/`pnpm` # earlier in PATH shadows the pins below, and `pnpm install` then runs under the wrong pnpm major. activate_aggressive = true [tools] gradle = "8.14.5" # Stock JDK 24 — the floor for this build. Bytecode targets 24 (--release 24), so an older JDK fails # to compile; the analyzer also calls sqlglot-go's probe via the Foreign Function & Memory API # (java.lang.foreign), which JDK 24 provides. java = "temurin-24.0.2" # Go builds the analyzer's c-shared native lib (analyzer/cmd/libanalyzer, via cgo) as part of the # Gradle build; matches analyzer/go.mod. (Zig — the Linux cross C compiler for the all-targets fat-jar # build, -Psqlglot.native.all=true — is only needed for that; add it when CI is wired.) go = "1.26.10" # Go protobuf generation. Buf provides the compiler; the two local plugins emit the checked-in Go files. buf = "1.72.0" protoc-gen-go = "1.35.2" protoc-gen-go-grpc = "1.5.1" # cp-go's typed store queries (cpgo/sqlc.yaml → cpgo/store/db). sqlc = "1.31.1" # web/ (Next.js). Pinned rather than "whatever's on PATH" so `pnpm install`'s lockfile resolution # and `next build` behavior stay reproducible across machines. Match web/Dockerfile's base image + # corepack pin when bumping either. node = "24.22.2" pnpm = "10.22.0" # auto-approver/ (pmon-auto-approver-agent): tests, and the self-contained binary the server image ships. Match the # oven/bun image in control-plane/Dockerfile when bumping. bun = "1.3.14" # Tasks. `mise run dev` is the whole local stack; `mise run verify` is the whole verification gate. # Each component also has its own task, so you can run one against a stack you started by hand. # # Every task runs from the repo root with the pinned toolchain on PATH, so `mise run ` works # from any subdirectory and no task needs to cd into a module. Every tunable reads through # `${VAR:-default}`, so an exported env var beats the default here — e.g. # `PM_HTTP_PORT=41390 mise run control-plane`. `mise tasks ls` lists them. [tasks.up] description = "Start the local datastores (control-plane store + sample MySQL/PG target DBs); waits until healthy" shell = "bash -c" # Idempotent, and safe when two callers overlap: on an already-healthy stack this only reads state. Two # concurrent `docker compose up` runs on one project fight over the same containers, and the loser tears # down what the winner just started. # mise renders every task script through a template engine first, so this script stays free of the # brace syntax that engine claims -- hence counting healthy services out of `ps --format json` rather # than formatting them with a Go template. run = ''' set -euo pipefail want=$(docker compose config --services | grep -c .) healthy=$(docker compose ps --format json | tr ',' '\n' | grep -c '"Health":"healthy"' || true) if [ "$want" -gt 0 ] && [ "$want" -eq "$healthy" ]; then echo "up: datastores already healthy" exit 0 fi docker compose up -d --wait ''' [tasks.down] description = "Stop the local datastores (their volumes survive)" run = "docker compose down" [tasks.control-plane] description = "Run the control plane (cp-go in front of the Kotlin control plane) against the local store — HTTP :8080, gRPC :9090" depends = ["up"] shell = "bash -c" # PM_CP_ATTACH=true skips starting Kotlin, for one you run yourself (e.g. from the IDE) with # PM_HTTP_PORT=18090 PM_GRPC_PORT=18091 PM_BIND_HOST=127.0.0.1 PM_TRUSTED_PROXIES=127.0.0.1, and the # same PM_CP_INTERNAL_TOKEN in both processes; set PM_CP_GO_GRPC_PORT here and # PM_CP_GO_GRPC_ADDR=127.0.0.1: there for the Go services Kotlin calls. run = ''' set -euo pipefail export PM_DB_URL="${PM_DB_URL:-jdbc:postgresql://localhost:5442/proxymonster}" if [ "${PM_CP_ATTACH:-}" != "true" ]; then ./gradlew --no-daemon -q :control-plane:installDist fi (cd cpgo && go build -o ../control-plane/build/cp-go ./cmd/cp-go) PM_CP_CHILD="$PWD/control-plane/build/install/control-plane/bin/control-plane" exec control-plane/build/cp-go ''' [tasks.proxy-mysql] description = "Run a wire proxy on :6033 fronting the sample MySQL target DB (datasource acme-mysql)" depends = ["up"] shell = "bash -c" run = ''' PM_ENGINE=mysql \ PM_DATASOURCE_NAME="${PM_DATASOURCE_NAME:-acme-mysql}" \ PM_PROXY_PORT="${PM_PROXY_PORT:-6033}" \ PM_TARGET_HOST="${PM_TARGET_HOST:-127.0.0.1}" \ PM_TARGET_PORT="${PM_TARGET_PORT:-31307}" \ PM_TARGET_DB="${PM_TARGET_DB:-acme}" \ PM_TARGET_USER="${PM_TARGET_USER:-acme}" \ PM_TARGET_PASSWORD="${PM_TARGET_PASSWORD:-acme}" \ PM_CONTROL_PLANE_GRPC="${PM_CONTROL_PLANE_GRPC:-127.0.0.1:9090}" \ go run ./goproxy/cmd/goproxy ''' [tasks.proxy-postgres] description = "Run a wire proxy on :6432 fronting the sample PostgreSQL target DB (datasource acme-target)" depends = ["up"] shell = "bash -c" run = ''' PM_ENGINE=postgres \ PM_DATASOURCE_NAME="${PM_DATASOURCE_NAME:-acme-target}" \ PM_PROXY_PORT="${PM_PROXY_PORT:-6432}" \ PM_TARGET_HOST="${PM_TARGET_HOST:-127.0.0.1}" \ PM_TARGET_PORT="${PM_TARGET_PORT:-5433}" \ PM_TARGET_DB="${PM_TARGET_DB:-acme}" \ PM_TARGET_USER="${PM_TARGET_USER:-acme}" \ PM_TARGET_PASSWORD="${PM_TARGET_PASSWORD:-acme}" \ PM_CONTROL_PLANE_GRPC="${PM_CONTROL_PLANE_GRPC:-127.0.0.1:9090}" \ go run ./goproxy/cmd/goproxy ''' [tasks.web] description = "Run the web console on :41300 against the local control plane" shell = "bash -c" # web/next.config.ts defaults PM_CP_URL to :41390; the control-plane task serves :8080, so point # the console there. run = ''' set -euo pipefail pnpm -C web install PM_CP_URL="${PM_CP_URL:-${PM_PROXY_TARGET:-http://127.0.0.1:8080}}" pnpm -C web dev ''' [tasks.dev] description = "The whole local stack: datastores, control plane, both wire proxies, web console" shell = "bash -c" run = ''' set -euo pipefail cp_url="${PM_CONTROL_PLANE_HTTP:-http://127.0.0.1:8080}" # Datastores first: the control plane needs its store, and each proxy introspects its target on boot. mise run up pids=() cleanup() { trap - EXIT INT TERM for pid in "${pids[@]:-}"; do kill "$pid" 2>/dev/null || true; done wait 2>/dev/null || true } trap cleanup EXIT INT TERM start() { # start [VAR=VAL …] local task="$1" shift env "$@" mise run "$task" & pids+=("$!") } # Each proxy registers with the control plane on boot and gives up after three tries, so wait for it. start control-plane echo "dev: waiting for the control plane at ${cp_url}/health …" for _ in $(seq 1 150); do curl -fsS "${cp_url}/health" >/dev/null 2>&1 && break sleep 2 done curl -fsS "${cp_url}/health" >/dev/null # Give each proxy its own port and target explicitly. Both read the same PM_PROXY_PORT/PM_TARGET_PORT # names, so one inherited value would otherwise land both on one port -- which is also why the # per-proxy override knobs below are separate names. MySQL is the primary target, so it leads. start proxy-mysql \ PM_PROXY_PORT="${PM_MYSQL_PROXY_PORT:-6033}" \ PM_TARGET_PORT="${PM_MYSQL_TARGET_PORT:-31307}" start proxy-postgres \ PM_PROXY_PORT="${PM_PG_PROXY_PORT:-6432}" \ PM_TARGET_PORT="${PM_PG_TARGET_PORT:-5433}" # Hand web the same control-plane URL this task just waited on, so the console's /api and /auth rewrite # can never point somewhere else. start web PM_CP_URL="${PM_CP_URL:-${PM_PROXY_TARGET:-$cp_url}}" cat < cpgo/sqlc.schema.tmp awk 'NR == FNR {list = list $0 "\n"; next} /^ schema:$/ {print; printf "%s", list; skip = 1; next} skip && /^ - / {next} {skip = 0; print}' \ cpgo/sqlc.schema.tmp cpgo/sqlc.yaml > cpgo/sqlc.yaml.new mv cpgo/sqlc.yaml.new cpgo/sqlc.yaml rm cpgo/sqlc.schema.tmp (cd cpgo && sqlc generate) ''' [tasks.sqlc-check] description = "Verify cp-go's sqlc schema lists every migration and the generated store code is current" shell = "bash -c" run = ''' set -euo pipefail want=$(ls control-plane/src/main/resources/db/migration | sort -t_ -k1.2 -n | sed 's#^# - ../control-plane/src/main/resources/db/migration/#') if [ "$want" != "$(grep '^ - ../control-plane/' cpgo/sqlc.yaml)" ]; then echo "cpgo/sqlc.yaml does not list the current migrations; run: mise run sqlc-generate" >&2 exit 1 fi (cd cpgo && sqlc diff) || { echo "cp-go store code is stale; run: mise run sqlc-generate" >&2; exit 1; } ''' [tasks.proto-check] description = "Verify the checked-in Go protobuf bindings are current" shell = "bash -c" run = ''' set -euo pipefail files=( analyzer/probe/pb/analyzer.pb.go analyzer/probe/pb/engine.pb.go analyzer/probe/pb/athena.pb.go analyzer/probe/pb/engine_call.pb.go goproxy/internal/pb/controlplane.pb.go goproxy/internal/pb/controlplane_grpc.pb.go cpgo/grpcsvc/pb/controlplane.pb.go cpgo/grpcsvc/pb/controlplane_grpc.pb.go cpgo/gobridge/pb/github_source.pb.go cpgo/gobridge/pb/github_source_grpc.pb.go ) before=$(git hash-object "${files[@]}") mise run proto-generate after=$(git hash-object "${files[@]}") if [ "$before" != "$after" ]; then git diff -- "${files[@]}" echo "Go protobuf bindings are stale; run: mise run proto-generate" >&2 exit 1 fi ''' [tasks.lint] description = "Lint: prettier over sources, docs and config (ts, js, md, json, yaml, css) + eslint over the web sources" shell = "bash -c" # Prettier owns formatting and eslint the Next.js rule set. Prettier is invoked by path from the repo # root rather than through `pnpm -C web exec`, because its scope is the whole tree and both # `.prettierrc.json` and `.prettierignore` are resolved relative to the working directory. `--write` # instead of `--check` reformats in place. run = ''' set -euo pipefail pnpm -C web install ./web/node_modules/.bin/prettier --check '**/*.{ts,tsx,js,mjs,cjs,md,json,yml,yaml,css}' pnpm -C web lint ''' [tasks.format] description = "Reformat the files prettier owns (ts, js, md, json, yaml, css) in place" shell = "bash -c" run = ''' set -euo pipefail pnpm -C web install ./web/node_modules/.bin/prettier --write '**/*.{ts,tsx,js,mjs,cjs,md,json,yml,yaml,css}' ''' [tasks.verify] description = "The full verification gate: lint, JVM tests, Go tests, web unit tests + build" shell = "bash -c" # The three parts run concurrently, each into its own log; CI runs the same three as parallel jobs, # sharding the two test tasks. PM_VERIFY_LOGS picks the log directory. The JVM tests take longest, so # the other two run at a lower CPU priority. run = ''' set -uo pipefail logs="${PM_VERIFY_LOGS:-$(mktemp -d "${TMPDIR:-/tmp}/pm-verify.XXXXXX")}" mkdir -p "$logs" echo "verify: logs in $logs" # The Go tests' fixed-name containers are otherwise shared with, and reaped by, other checkouts' runs. export PM_TEST_CONTAINER_SUFFIX="verify-$$-$(date +%s)" trap 'docker rm -fv ${pg:-} ${mysql:-} $(docker ps -aq --filter "name=-$PM_TEST_CONTAINER_SUFFIX") >/dev/null 2>&1' EXIT # The JVM test forks share one database server per engine (TestDatabases.kt) instead of each starting its own. pg=$(docker run -d --rm -P -e POSTGRES_USER=test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=test \ "${PM_TEST_POSTGRES_IMAGE:-postgres:17}" postgres -c max_connections=2000) || exit 1 trap 'kill $(jobs -p) 2>/dev/null; wait; exit 130' INT TERM # Durability only: one server takes every fork's DDL, which otherwise queues on its fsyncs. mysql=$(docker run -d --rm -P -e MYSQL_ROOT_PASSWORD=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test -e MYSQL_DATABASE=test \ "${PM_TEST_MYSQL_IMAGE:-mysql:8.4}" --max-connections=1000 --innodb-flush-log-at-trx-commit=2 --sync-binlog=0) || exit 1 pg_port=$(docker port "$pg" 5432/tcp | head -1) mysql_port=$(docker port "$mysql" 3306/tcp | head -1) export PM_TEST_POSTGRES_SERVER="localhost:${pg_port##*:}" PM_TEST_MYSQL_SERVER="localhost:${mysql_port##*:}" parts="static jvm go" start=$SECONDS # Set here rather than in the build so CI keeps its own; past 3/4 of Docker's CPUs more forks gained nothing. docker_cpus=$(docker info | awk '/^ CPUs:/ {print $2}') forks=$((docker_cpus * 3 / 4)) host_cpus=$(getconf _NPROCESSORS_ONLN) [ "$forks" -gt "$host_cpus" ] && forks=$host_cpus [ "$forks" -lt 1 ] && forks=1 export GRADLE_OPTS="${GRADLE_OPTS:-} -Dorg.gradle.parallel=true -Dorg.gradle.project.pm.testForks=$forks" for part in $parts; do ( t0=$SECONDS if [ "$part" = jvm ]; then prio=0; else prio=10; fi nice -n "$prio" mise run "verify-$part" > "$logs/$part.log" 2>&1 rc=$? echo "$rc" > "$logs/$part.status" if [ "$rc" -eq 0 ]; then result=passed; else result=FAILED; fi echo "verify: $part $result in $((SECONDS - t0))s" ) & done wait failed="" for part in $parts; do rc=$(cat "$logs/$part.status" 2>/dev/null || echo 1) if [ "$rc" -ne 0 ]; then failed="$failed $part" echo "=== verify-$part failed; last 60 lines of $logs/$part.log ===" >&2 tail -n 60 "$logs/$part.log" >&2 fi done if [ -n "$failed" ]; then echo "verify: FAILED:$failed after $((SECONDS - start))s" >&2 exit 1 fi echo "verify: all parts passed in $((SECONDS - start))s" ''' [tasks.verify-static] description = "The gate's non-test checks: lint, proto bindings, image go.mod builds, web unit tests + build" shell = "bash -c" run = ''' set -euo pipefail mise run lint mise run proto-check mise run sqlc-check mise run check-image-gomod pnpm -C web test:unit pnpm -C web build mise run verify-auto-approver ''' [tasks.verify-auto-approver] description = "pmon-auto-approver-agent: install, typecheck, tests, and the compiled binary the image ships" shell = "bash -c" dir = "{{config_root}}/auto-approver" run = ''' set -euo pipefail bun install --frozen-lockfile bun run typecheck bun test bun run build ''' [tasks.verify-jvm] description = "The gate's JVM tests (PM_TEST_SHARD=i/n runs one slice of the test classes)" shell = "bash -c" # The JVM tests are DB-backed (Testcontainers) and fail rather than skip, so Docker must be running. # --no-daemon: a shared Gradle daemon is machine-wide state, so a build here can recycle a JVM another # process depends on. Without it, this gate is not self-contained. run = ''' set -euo pipefail ./gradlew --no-daemon ${PM_TEST_SHARD:+-Ppm.testShard=$PM_TEST_SHARD} :analyzer:jvm:test :engine:test :control-plane:test ''' [tasks.verify-go] description = "The gate's Go tests (PM_GO_PACKAGES narrows them to some module patterns)" shell = "bash -c" # Explicit module patterns: the repo root is a Go workspace, not a module, so `./...` does not resolve # there. go.work is what makes these root-relative paths work with no cd. # # -race: the concurrency this proxy is built on — the events loop, the per-connection goroutines — is # where a race is both most likely and least visible, and a data race is a correctness bug the plain # run reports as a pass. These tests wait on containers and sockets far more than they compute, so the # detector costs a few seconds of wall clock rather than the usual multiple. # # The packages share one testcontainers reaper, which exits 10s after its last package lets go; a package # starting in that window waits on the dying reaper until its 60s deadline. run = ''' set -euo pipefail pkgs=${PM_GO_PACKAGES:-./analyzer/... ./auditmon/... ./cpgo/... ./goproxy/... ./mysqlwire/... ./pmon/... ./pmon/tray/... ./sql-action/...} TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT=5m go test -race $pkgs ''' [tasks.check-image-gomod] description = "Build each server-image Go module in go.mod mode (GOWORK=off), the way the image build does" shell = "bash -c" # The server images (goproxy/Dockerfile, auditmon/Dockerfile, control-plane/Dockerfile's cp-go) build each module GOWORK=off, so it # resolves its OWN go.mod — not the workspace. Every other Go build here goes through go.work, which # resolves a sibling like analyzer from ../analyzer and hides a stale transitive pin in goproxy/go.mod: # the drift then surfaces only on the release image build (a broken pm-goproxy), never in `verify`. # Reproduce that resolution here so a stale pin fails on the author's machine and in PR CI instead. # `go build ./...` compiles every package and writes no binary. run = ''' set -euo pipefail for m in goproxy auditmon cpgo; do echo "=== $m (GOWORK=off go build ./...) ===" (cd "$m" && GOWORK=off go build ./...) done ''' [tasks.build-pmon] description = "Build pmon into ./dist, stamped with the commit it came from" shell = "bash -c" # Go stamps vcs.revision on its own, but skips it while a go.work is active — which is always, here. # So the revision is passed explicitly, with a .dirty suffix when the tree has uncommitted changes: a # binary built from a modified tree matches no commit, and `pmon --version` should say so rather than # name one it was not built from. run = ''' set -euo pipefail rev=$(git rev-parse --short=12 HEAD) git diff --quiet && git diff --cached --quiet || rev="$rev.dirty" mkdir -p dist (cd pmon && go build -ldflags "-X main.Commit=$rev" -o ../dist/pmon .) echo "built dist/pmon — $(./dist/pmon --version)" ''' [tasks.test-jvm] description = "JVM tests against one database version (defaults to the newest supported)" run = "./gradlew --no-daemon test" [tasks.test-go] description = "Go tests against one database version (defaults to the newest supported)" shell = "bash -c" # Each Go module is entered explicitly rather than using a root `./...`: the repo root is not a Go # module, so a root-relative pattern only resolves once a go.work exists. run = ''' set -euo pipefail for m in analyzer auditmon cpgo goproxy mysqlwire pmon pmon/tray sql-action; do echo "=== $m ===" (cd "$m" && go test -race ./...) done ''' [tasks.test] description = "All tests against the newest supported database version — the local default" depends = ["test-jvm", "test-go"] [tasks.test-e2e-clients] description = "Client-interop e2e: real MySQL CLI/JDBC/Go/Node/PHP and PostgreSQL psql/pgJDBC/pgx/Node clients connect through pmon" shell = "bash -c" # Behind the e2e_clients build tag and out of `verify`: it pulls several client images and needs outbound # network (JDBC jars and npm installs). Docker is required. run = "(cd pmon && go test -race -tags e2e_clients ./internal/daemon/ -run ClientInterop -count=1 -timeout 900s)" [tasks.test-db-matrix] description = "Sweep the DB-backed tests over EVERY supported database version (db-support.json)" shell = "bash -c" # Sequential by default — one database container at a time, and the ordinary `build/` tree, so the run # is easy to follow and leaves the incremental build cache warm. `PM_MATRIX_PARALLEL=1` (or # `mise run test-db-matrix-parallel`) runs every leg at once instead; see that task for what isolation # it needs. Legs are not aborted on the first failure — which versions fail is the useful output — but # any failure fails the task. run = ''' set -uo pipefail if [ "${PM_MATRIX_PARALLEL:-0}" != "0" ]; then exec mise run test-db-matrix-parallel fi pgs=$(jq -r '[.storage[] | select(.engine == "postgres") | .image] | join(" ")' db-support.json) mysqls=$(jq -r '[.target[] | select(.engine == "mysql") | .image] | join(" ")' db-support.json) if [ -z "$pgs" ] || [ -z "$mysqls" ]; then echo "db-support.json yielded no versions to test" >&2 exit 1 fi failed="" for pg in $pgs; do for my in $mysqls; do echo "=== $pg + $my ===" if ! PM_TEST_POSTGRES_IMAGE="$pg" PM_TEST_MYSQL_IMAGE="$my" PM_REQUIRE_DB_TESTS=true \ ./gradlew --no-daemon :control-plane:test; then failed="$failed jvm($pg,$my)" fi if ! (cd goproxy && PM_TEST_POSTGRES_IMAGE="$pg" PM_TEST_MYSQL_IMAGE="$my" go test -race ./...); then failed="$failed goproxy($pg,$my)" fi done if ! (cd auditmon && PM_TEST_POSTGRES_IMAGE="$pg" go test -race ./...); then failed="$failed auditmon($pg)" fi done if [ -n "$failed" ]; then echo "FAILED legs:$failed" >&2 exit 1 fi echo "all supported database versions passed" ''' [tasks.test-db-matrix-parallel] description = "Same sweep, version legs run concurrently (PM_MATRIX_JOBS caps how many at once)" shell = "bash -c" # Concurrent legs. Three things make that work, and all are required: # # -Ppm.buildDir — two Gradle invocations in one project directory fight over `build/`: the second # deletes the classes the first is compiling ("Unable to delete directory # .../classes/kotlin/test"). Each leg gets its own build tree. # --project-cache-dir — likewise per-leg, so the legs do not block on one lock. # PM_MATRIX_JOBS — a concurrency CAP, because the binding constraint is the DOCKER VM, not the host. # Host RAM is irrelevant here: Docker Desktop's VM gets a fixed slice of it (often # ~8 GB on a machine with 64 — check `docker info`), and every JVM leg starts its own # PostgreSQL AND MySQL server inside that slice. N legs means 2N database servers. # Past the VM's budget they do not fail cleanly: connections are refused mid-suite # and it surfaces as "Communications link failure" / ConnectException, which reads # like a product bug rather than exhaustion. # # Default cap is 2 legs (4 database servers), which fits an ~8 GB VM. Raising it needs VM memory, not # host memory — measured on a 7.75 GiB VM, 4 legs (8 servers) fails. To go wider, raise Docker # Desktop's memory limit first, then: PM_MATRIX_JOBS=4 mise run test-db-matrix-parallel # PM_MATRIX_HEAP tunes each leg's Gradle heap (default 3g) if the Kotlin compile is what runs short. # # Output goes to per-leg log files; interleaved stdout from several suites at once is unreadable. A # leg's log is printed only if it fails. run = ''' set -uo pipefail jobs_cap="${PM_MATRIX_JOBS:-2}" case "$jobs_cap" in ''|*[!0-9]*|0) echo "PM_MATRIX_JOBS must be a positive integer" >&2; exit 1;; esac legs=$(jq -r ' [ .storage[] | select(.engine == "postgres") | .image ] as $pg | [ .target[] | select(.engine == "mysql") | .image ] as $my | [ $pg[] as $p | $my[] as $m | "\($p) \($m)" ] | .[] ' db-support.json) pgs=$(jq -r '[.storage[] | select(.engine == "postgres") | .image] | join(" ")' db-support.json) if [ -z "$legs" ] || [ -z "$pgs" ]; then echo "db-support.json yielded no versions to test" >&2 exit 1 fi logs=$(mktemp -d) echo "logs: $logs" # mise renders this script through a template engine first, so no Go-template braces here — the # VM's memory is read out of `docker info`'s plain output instead of formatted. echo "docker vm memory: $(docker info 2>/dev/null | sed -n 's/^ *Total Memory: *//p')" # Wait until fewer than $jobs_cap background jobs remain. `wait -n` returns on the first one to # finish, so this throttles without polling. A leg records its own failure to $logs/failed, so a # non-zero exit here is not the signal and must not abort the sweep. throttle() { while [ "$(jobs -pr | wc -l)" -ge "$jobs_cap" ]; do wait -n || true; done } n=0 while read -r pg my; do [ -n "$pg" ] || continue n=$((n + 1)) tag="${pg//[:\/]/-}__${my//[:\/]/-}" throttle echo "--> leg $n: $pg + $my" ( # Each leg's build tree is cold, so every leg compiles Kotlin from scratch. Concurrent --no-daemon # builds get the JVM's default heap, which is not enough for two Kotlin backends at once: the # compiler dies with "OutOfMemoryError: GC overhead limit exceeded" mid-codegen, which reads like a # source error rather than a resource one. Give each leg an explicit heap. PM_TEST_POSTGRES_IMAGE="$pg" PM_TEST_MYSQL_IMAGE="$my" PM_REQUIRE_DB_TESTS=true \ GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx${PM_MATRIX_HEAP:-3g}" \ ./gradlew --no-daemon --no-build-cache -Ppm.buildDir=".gradle-matrix/$tag" \ --project-cache-dir "$logs/gradle-$tag" :control-plane:test \ > "$logs/jvm-$tag.log" 2>&1 || echo "jvm($pg,$my)" >> "$logs/failed" (cd goproxy && PM_TEST_POSTGRES_IMAGE="$pg" PM_TEST_MYSQL_IMAGE="$my" \ PM_TEST_CONTAINER_SUFFIX="$tag" go test -race ./...) \ > "$logs/goproxy-$tag.log" 2>&1 || echo "goproxy($pg,$my)" >> "$logs/failed" ) & done <<< "$legs" # auditmon is PostgreSQL-only, so it is one leg per PostgreSQL version rather than per pair. for pg in $pgs; do throttle echo "--> auditmon leg: $pg" ( (cd auditmon && PM_TEST_POSTGRES_IMAGE="$pg" PM_TEST_CONTAINER_SUFFIX="am-${pg//[:\/]/-}" go test -race ./...) \ > "$logs/auditmon-${pg//[:\/]/-}.log" 2>&1 || echo "auditmon($pg)" >> "$logs/failed" ) & done wait if [ -f "$logs/failed" ]; then echo "FAILED legs:" >&2 sort -u "$logs/failed" >&2 # Print the failing legs' output; a bare list of names is not actionable. Match compile/infra failures # too, not just test failures — a leg that never got as far as running a test is the most confusing # kind to debug from names alone. for f in "$logs"/*.log; do grep -qE "^FAIL|^--- FAIL|BUILD FAILED|^e: |OutOfMemoryError" "$f" && { echo "--- $f ---" >&2 grep -E "^FAIL|^--- FAIL|BUILD FAILED|^e: |OutOfMemoryError|^\s+[A-Za-z].*: " "$f" | head -25 >&2 } done echo >&2 echo "If failures look like refused/dropped connections rather than assertion failures, the Docker" >&2 echo "VM is likely out of memory for $jobs_cap concurrent legs ($((jobs_cap * 2)) database servers)." >&2 echo "Lower PM_MATRIX_JOBS or raise Docker Desktop's memory limit." >&2 exit 1 fi echo "all supported database versions passed ($n pairs, up to $jobs_cap legs at a time)" ''' [tasks.smoke] description = "End-to-end smoke: own compose project + fresh control plane, proxies, pmon; PASS/FAIL per engine and leg" run = "smoke/run.sh"