Files
lai_automatisierung/old_2026-08-31/src/lai_automation/config.py
2026-08-31 02:46:58 +02:00

142 lines
6.1 KiB
Python

from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
_DEFAULT_SETUP_COMMANDS = (
"composer install --no-interaction --prefer-dist;;"
"npm ci;;"
"npm run build;;"
"php artisan migrate:fresh --force"
)
def _bool(name: str, default: bool = False) -> bool:
return os.getenv(name, str(default)).lower() in {"1", "true", "yes", "on"}
def _commands(value: str) -> tuple[str, ...]:
return tuple(command.strip() for command in value.split(";;") if command.strip())
@dataclass(frozen=True)
class Settings:
database_host: str
database_port: int
database_name: str
database_user: str
database_password: str
source_repository: str
source_branch: str
workspace_root: Path
mode: str
allow_direct_main: bool
gitea_url: str
gitea_owner: str
gitea_repository: str
gitea_token: str
gitea_username: str
agent_command: str
agent_model: str
agent_timeout_seconds: int
agent_idle_timeout_seconds: int
setup_commands: tuple[str, ...]
test_commands: tuple[str, ...]
test_database_host: str
test_database_port: int
test_database_name: str
test_database_user: str
test_database_password: str
test_customer_database: str
test_config_database: str
poll_seconds: int
max_changed_files: int
max_changed_lines: int
max_agent_attempts: int
stale_claim_minutes: int
dry_run: bool
@classmethod
def from_env(cls) -> Settings:
mode = os.getenv("AUTOMATION_MODE", "dry-run").lower()
if mode not in {"dry-run", "pr", "direct-main"}:
raise ValueError("AUTOMATION_MODE must be dry-run, pr, or direct-main")
settings = cls(
database_host=os.getenv("FAILURE_DB_HOST", "host.docker.internal"),
database_port=int(os.getenv("FAILURE_DB_PORT", "33060")),
database_name=os.getenv("FAILURE_DB_NAME", "laravel_ai_chat"),
database_user=os.getenv("FAILURE_DB_USER", "laravel"),
database_password=os.getenv("FAILURE_DB_PASSWORD", ""),
source_repository=os.getenv(
"SOURCE_REPOSITORY", "https://gitea.tisinfra.de/christian.meyer/lai.git"
),
source_branch=os.getenv("SOURCE_BRANCH", "main"),
workspace_root=Path(os.getenv("WORKSPACE_ROOT", "/workspaces")),
mode=mode,
allow_direct_main=_bool("ALLOW_DIRECT_MAIN"),
gitea_url=os.getenv("GITEA_URL", "https://gitea.tisinfra.de"),
gitea_owner=os.getenv("GITEA_OWNER", "christian.meyer"),
gitea_repository=os.getenv("GITEA_REPOSITORY", "lai"),
gitea_token=os.getenv("GITEA_TOKEN", ""),
gitea_username=os.getenv("GITEA_USERNAME", "automation-bot"),
agent_command=os.getenv("AGENT_COMMAND", "aider"),
agent_model=os.getenv("AGENT_MODEL", "mistral/devstral-latest"),
agent_timeout_seconds=int(os.getenv("AGENT_TIMEOUT_SECONDS", "900")),
agent_idle_timeout_seconds=int(os.getenv("AGENT_IDLE_TIMEOUT_SECONDS", "300")),
setup_commands=_commands(
os.getenv("SETUP_COMMANDS", _DEFAULT_SETUP_COMMANDS)
),
test_commands=_commands(
os.getenv("TEST_COMMANDS", "php artisan test")
),
test_database_host=os.getenv("TEST_DB_HOST", "host.docker.internal"),
test_database_port=int(os.getenv("TEST_DB_PORT", "33060")),
test_database_name=os.getenv("TEST_DB_NAME", "laravel_ai_chat_agent_test"),
test_database_user=os.getenv("TEST_DB_USER", "lai_test_runner"),
test_database_password=os.getenv("TEST_DB_PASSWORD", ""),
test_customer_database=os.getenv("TEST_CUSTOMER_DB", "customer_agent_test"),
test_config_database=os.getenv("TEST_CONFIG_DB", "cockpit_agent_test"),
poll_seconds=int(os.getenv("POLL_SECONDS", "60")),
max_changed_files=int(os.getenv("MAX_CHANGED_FILES", "12")),
max_changed_lines=int(os.getenv("MAX_CHANGED_LINES", "800")),
max_agent_attempts=int(os.getenv("MAX_AGENT_ATTEMPTS", "2")),
stale_claim_minutes=int(os.getenv("STALE_CLAIM_MINUTES", "90")),
dry_run=mode == "dry-run",
)
settings.validate()
return settings
def validate(self) -> None:
if self.mode == "direct-main" and not self.allow_direct_main:
raise ValueError("direct-main requires ALLOW_DIRECT_MAIN=true")
if self.mode in {"pr", "direct-main"} and not self.gitea_token:
raise ValueError(f"{self.mode} requires GITEA_TOKEN")
if not self.database_password:
raise ValueError("FAILURE_DB_PASSWORD is required")
if not self.test_database_password:
raise ValueError("TEST_DB_PASSWORD is required")
if not self.test_commands:
raise ValueError("TEST_COMMANDS must contain at least one command")
test_databases = {
"TEST_DB_NAME": self.test_database_name,
"TEST_CUSTOMER_DB": self.test_customer_database,
"TEST_CONFIG_DB": self.test_config_database,
}
for setting_name, database_name in test_databases.items():
if "test" not in database_name.lower():
raise ValueError(f"{setting_name} must name an isolated test database")
if database_name == self.database_name:
raise ValueError(f"{setting_name} must differ from FAILURE_DB_NAME")
if len(set(test_databases.values())) != len(test_databases):
raise ValueError("TEST_DB_NAME, TEST_CUSTOMER_DB, and TEST_CONFIG_DB must be distinct")
if not 60 <= self.agent_timeout_seconds <= 3600:
raise ValueError("AGENT_TIMEOUT_SECONDS must be between 60 and 3600")
if not 60 <= self.agent_idle_timeout_seconds <= self.agent_timeout_seconds:
raise ValueError(
"AGENT_IDLE_TIMEOUT_SECONDS must be between 60 and AGENT_TIMEOUT_SECONDS"
)
if not 1 <= self.max_agent_attempts <= 7:
raise ValueError("MAX_AGENT_ATTEMPTS must be between 1 and 7")