From 696d47d07fc87ffe15b4863266e40e96908153f7 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Mon, 21 Sep 2026 11:26:34 +0200 Subject: [PATCH 1/2] Add PGDEVKIT_TESTDB_BRANCH override for concurrent CI checkouts current_branch() falls back to `git rev-parse --abbrev-ref HEAD`, which returns the literal "HEAD" for every detached-HEAD checkout. Independent CI checkouts (not git worktrees of one shared repo) all land in that state, so concurrent jobs on a persistent/self-hosted runner sharing one test-DB container collide on the identical workspace_db_name. Setting PGDEVKIT_TESTDB_BRANCH to a per-job value (e.g. a CI job id) gives each job its own database instead. Bump version to 0.7.2. --- pgdevkit/testdb/naming.py | 22 +++++++++++++++++++++- pyproject.toml | 2 +- tests/testdb/test_naming.py | 8 ++++++++ uv.lock | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/pgdevkit/testdb/naming.py b/pgdevkit/testdb/naming.py index a954b8c..a6c0ad5 100644 --- a/pgdevkit/testdb/naming.py +++ b/pgdevkit/testdb/naming.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import os import re import subprocess from pathlib import Path @@ -25,7 +26,26 @@ def slugify(value: str) -> str: def current_branch(cwd: Path | None = None) -> str: - """Return the branch checked out in the git worktree rooted at cwd.""" + """Return the branch checked out in the git worktree rooted at cwd. + + PGDEVKIT_TESTDB_BRANCH overrides this when set. CI checkouts are + typically independent, non-worktree clones that each land in a detached + HEAD, so `git rev-parse --abbrev-ref HEAD` returns the literal "HEAD" in + every one of them; on a persistent/self-hosted runner where several such + checkouts can run concurrently against the same shared test-DB + container, that collapses every job onto the identical + workspace_db_name and they stomp each other's database. Set this to + something unique per job (e.g. a CI job id) to give each one its own + database instead -- note that doing so also opts that database out of + live_worktree_branches' detached-HEAD protection (see its docstring), + so an override-named database always looks orphaned to + find_orphaned_dbs/clean_testdb(orphaned=True); that's intentional for + ephemeral CI jobs, which should just clean up their own database + directly (clean_testdb()) rather than rely on orphan sweeping. + """ + override = os.environ.get("PGDEVKIT_TESTDB_BRANCH") + if override: + return override result = subprocess.run( ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, diff --git a/pyproject.toml b/pyproject.toml index 0949bec..7217cb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ packages = ["pgdevkit"] [project] name = "pgdevkit" -version = "0.7.1" +version = "0.7.2" description = "A helper for developing with Postgres" readme = "README.md" requires-python = ">=3.14" diff --git a/tests/testdb/test_naming.py b/tests/testdb/test_naming.py index 6a71ebd..66cd380 100644 --- a/tests/testdb/test_naming.py +++ b/tests/testdb/test_naming.py @@ -53,6 +53,14 @@ def test_current_branch_reads_the_checked_out_branch(tmp_path: Path): assert current_branch(tmp_path) == "my-feature" +def test_current_branch_env_override_skips_git_entirely(tmp_path: Path, monkeypatch): + # Not a git repo at all -- the override must short-circuit before the + # `git rev-parse` subprocess call, which would otherwise raise. + monkeypatch.setenv("PGDEVKIT_TESTDB_BRANCH", "ci-job-42") + + assert current_branch(tmp_path) == "ci-job-42" + + def _init_repo(repo: Path, initial_branch: str) -> None: repo.mkdir() subprocess.run(["git", "init", "-q"], cwd=repo, check=True) diff --git a/uv.lock b/uv.lock index cb04842..0fe71f7 100644 --- a/uv.lock +++ b/uv.lock @@ -313,7 +313,7 @@ wheels = [ [[package]] name = "pgdevkit" -version = "0.7.1" +version = "0.7.2" source = { editable = "." } dependencies = [ { name = "docker" }, From 9c324807353d9290ed133b70af754572423325fc Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Mon, 21 Sep 2026 11:26:54 +0200 Subject: [PATCH 2/2] Trim current_branch() docstring --- pgdevkit/testdb/naming.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/pgdevkit/testdb/naming.py b/pgdevkit/testdb/naming.py index a6c0ad5..44ca574 100644 --- a/pgdevkit/testdb/naming.py +++ b/pgdevkit/testdb/naming.py @@ -28,20 +28,9 @@ def slugify(value: str) -> str: def current_branch(cwd: Path | None = None) -> str: """Return the branch checked out in the git worktree rooted at cwd. - PGDEVKIT_TESTDB_BRANCH overrides this when set. CI checkouts are - typically independent, non-worktree clones that each land in a detached - HEAD, so `git rev-parse --abbrev-ref HEAD` returns the literal "HEAD" in - every one of them; on a persistent/self-hosted runner where several such - checkouts can run concurrently against the same shared test-DB - container, that collapses every job onto the identical - workspace_db_name and they stomp each other's database. Set this to - something unique per job (e.g. a CI job id) to give each one its own - database instead -- note that doing so also opts that database out of - live_worktree_branches' detached-HEAD protection (see its docstring), - so an override-named database always looks orphaned to - find_orphaned_dbs/clean_testdb(orphaned=True); that's intentional for - ephemeral CI jobs, which should just clean up their own database - directly (clean_testdb()) rather than rely on orphan sweeping. + PGDEVKIT_TESTDB_BRANCH overrides this when set -- lets concurrent CI + checkouts (which all otherwise resolve to detached-HEAD "HEAD") get + distinct database names on a shared test-DB container. """ override = os.environ.get("PGDEVKIT_TESTDB_BRANCH") if override: