fix: force interactive login shell for bash containers

Detached containers with tty: true still exited immediately because a
plain /bin/bash invocation exits with code 0 when stdin is not connected.

- Detect when the container CMD is /bin/bash or bash and exec an
  interactive login shell () after dropping privileges
- Keep the generic  path for non-shell commands
- Bump compiler_version to v4 to force a fresh image build

Quality gates:
- pytest tests/unit: 219 passed
- ruff: clean on changed files
- mypy: clean on changed files
This commit is contained in:
Developer
2026-06-15 11:18:28 +00:00
parent ed1f7805f6
commit 29e48cdb65
12 changed files with 24 additions and 16 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api
## role
FastAPI backend API that manages projects, git repositories, and development tools through Docker-based instances with PostgreSQL persistence.
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools by orchestrating Docker instances for remote development environments.
## parent
index: apps/.pi-map.index.md
map: apps/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/api
index: apps/api/.pi-map.index.md
## role
FastAPI backend API that manages projects, git repositories, and development tools through Docker-based instances with PostgreSQL persistence.
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools by orchestrating Docker instances for remote development environments.
## files
- .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and avoid copying unnecessary files into containers. | dep: Docker
- Dockerfile | Multi-stage Docker build for a Python application with Docker socket access, Cloudflare tunneling, and database dependency waiting | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies
@@ -14,7 +14,7 @@ FastAPI backend API that manages projects, git repositories, and development too
- uv.lock | Lock file for the uv Python package manager that pins exact dependency versions and their artifact hashes for reproducible installations | dep: uv, Python 3.11+, aiosqlite, alembic, annotated-doc, annotated-types, anyio, ast-serialize, asyncpg, and many other PyPI packages
- wait-for-db.sh | Wait for a PostgreSQL database to become available before executing a command, with configurable retry logic. | dep: nc (netcat), sh (POSIX shell), sleep
## arch
Async Python/FastAPI service using multi-stage Docker containers, Alembic migrations, uv package management, and external Docker socket access for container orchestration.
Async Python backend using FastAPI with PostgreSQL (asyncpg), Alembic migrations, uv package management, multi-stage Docker builds with Docker-in-Docker socket access, and Cloudflare tunneling for secure external connectivity.
## tags
docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn
## symbols
@@ -393,6 +393,13 @@ def compile_entrypoint(manifest: dict) -> str:
if user:
name = user["name"]
lines.append("# Drop privileges to the container user")
# When the container command is a shell, force an interactive login
# shell. Detached containers may not have stdin connected, and a plain
# /bin/bash invocation exits immediately with code 0. -il keeps it
# alive so the container stays running for docker exec/web terminals.
lines.append('if [ "$1" = "/bin/bash" ] || [ "$1" = "bash" ]; then')
lines.append(f' exec runuser -u {name} -- /bin/bash -il')
lines.append('fi')
lines.append(f'exec runuser -u {name} -- "$@"')
else:
lines.append('exec "$@"')
@@ -582,7 +589,7 @@ def compute_image_tag(tool_name: str, manifest: dict) -> str:
Returns:
Docker image tag string.
"""
compiler_version = "v3" # bump when compile_dockerfile/entrypoint/compose change
compiler_version = "v4" # bump when compile_dockerfile/entrypoint/compose change
canonical = json.dumps(manifest, sort_keys=True, separators=(",", ":"))
hash_suffix = hashlib.sha256(
f"{compiler_version}:{canonical}".encode()
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/tests
## role
Provides shared test infrastructure and fixtures for API integration testing
Provides shared test infrastructure and fixtures for the API application's test suite.
## parent
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
+2 -2
View File
@@ -4,11 +4,11 @@ dir: apps/api/tests
index: apps/api/tests/.pi-map.index.md
## role
Provides shared test infrastructure and fixtures for API integration testing
Provides shared test infrastructure and fixtures for the API application's test suite.
## files
- conftest.py | Provides shared pytest fixtures for testing a FastAPI application with async SQLite database, authenticated clients, and test data setup. | exp: func:test_client() → Generator[TestClient, None, None], call:create_async_engine, call:engine.begin, call:conn.run_sync, call:asyncio.run, call:init_db, call:async_sessionmaker, call:patch, call:TestClient, call:app.dependency_overrides.pop, call:engine.dispose, func:init_db(), call:engine.begin, call:conn.run_sync, func:override_get_db_session() → AsyncGenerator[AsyncSession, None], call:async_sessionmaker, func:db_session(test_client) → AsyncGenerator[AsyncSession, None], call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:gen.aclose, call:create_async_engine, call:engine.begin, call:conn.run_sync, call:async_sessionmaker, call:engine.dispose, func:authenticated_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_test_user, call:create_session_cookie, call:test_client.cookies.set, func:create_test_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, func:test_project_and_repo(authenticated_client) → tuple[str, str], call:uuid.uuid4, call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, call:asyncio.run, call:get_user_id, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, call:create_project_and_repo, call:str, raise:RuntimeError, func:get_user_id(), call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, func:create_project_and_repo(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, func:admin_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_admin_user, call:create_session_cookie, call:test_client.cookies.set, func:create_admin_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose | dep: asyncio, os, typing, unittest.mock, pytest, pytest_asyncio, fastapi.testclient, sqlalchemy.ext.asyncio, src.config, src.models.base, src.main, src.auth.dependencies, uuid, src.auth.session, src.models.user.user, src.models.project.project, src.models.project.git_repository, fastapi, sqlalchemy, aiosqlite, src.models, src.auth
## arch
Pytest fixture-based architecture with async SQLite test database, dependency injection overrides, and authenticated client factories for FastAPI endpoint testing
Pytest fixture-based testing architecture with async SQLite test database, dependency injection overrides, and authenticated client factories using FastAPI's TestClient.
## tags
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/tests/unit
## role
Contains comprehensive unit tests for the API application's core services, utilities, and infrastructure components.
Contains unit tests for the API application's core services, utilities, and infrastructure components.
## parent
index: apps/api/tests/.pi-map.index.md
map: apps/api/tests/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/api/tests/unit
index: apps/api/tests/unit/.pi-map.index.md
## role
Contains comprehensive unit tests for the API application's core services, utilities, and infrastructure components.
Contains unit tests for the API application's core services, utilities, and infrastructure components.
## files
- __init__.py | Empty file with no functionality
- test_alembic_migrations.py | Unit tests that verify Alembic database migrations are importable, have correct revision identifiers, and declare expected dependencies without requiring a live database. | exp: func:test_home_directory_migration_imports_and_rewrites() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_merge_migration_resolves_heads() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_remove_pi_agent_repo_mount_migration_imports() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable | dep: importlib.util, pathlib, pytest, importlib
@@ -30,7 +30,7 @@ Contains comprehensive unit tests for the API application's core services, utili
- test_readiness_probe.py | Unit tests for a Docker container readiness probe service that executes commands via docker exec with retry logic. | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.shared.readiness_probe, subprocess
- test_ssh_keys.py | Unit tests for SSH key preparation functionality including file creation, permissions, ownership, and error handling | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.shared.ssh_keys
## arch
Standard Python unittest/pytest pattern with heavy use of mocking (subprocess, filesystem, database) to isolate components; tests are organized by service/domain with each file targeting a single module/class; no internal dependencies between test files.
Comprehensive unit test suite using mocked dependencies to test business logic in isolation, covering database migrations, configuration, Docker operations, Git workflows, file services, event handling, health monitoring, and SSH/security utilities.
## tags
test, url, call:notification, git, call:, home, merge, call:db
## symbols
@@ -270,6 +270,7 @@ def test_compile_dockerfile_starts_as_root_and_drops_privileges() -> None:
entrypoint = compile_entrypoint(manifest)
assert "USER dev" not in dockerfile
assert 'exec runuser -u dev -- /bin/bash -il' in entrypoint
assert 'exec runuser -u dev -- "$@"' in entrypoint