ci: add GitHub Actions workflow with ruff and E2E tests

- Add CI workflow running Ruff lint/format checks
- Run full E2E cycle (Docker Compose + Playwright + tests)
- Refactor code formatting to satisfy Ruff (line breaks, readability)
- Use sys.executable in tests for interpreter-agnostic execution

https://chatgpt.com/share/694a7f81-d96c-800f-88cb-7b25b4cdfe1a
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-23 12:39:39 +01:00
parent 5dbb1857c9
commit 482ac3377d
6 changed files with 135 additions and 23 deletions

View File

@@ -4,4 +4,5 @@ Headless bootstrap tooling for Matomo:
- readiness checks
- admin/API token provisioning
"""
__all__ = []

View File

@@ -13,7 +13,9 @@ class Config:
token_description: str = "matomo-bootstrap"
timeout: int = 20
debug: bool = False
matomo_container_name: str | None = None # optional, for future console installer usage
matomo_container_name: str | None = (
None # optional, for future console installer usage
)
def config_from_env_and_args(args) -> Config:
@@ -21,9 +23,15 @@ def config_from_env_and_args(args) -> Config:
Build a Config object from CLI args (preferred) and environment variables (fallback).
"""
base_url = getattr(args, "base_url", None) or os.environ.get("MATOMO_URL")
admin_user = getattr(args, "admin_user", None) or os.environ.get("MATOMO_ADMIN_USER")
admin_password = getattr(args, "admin_password", None) or os.environ.get("MATOMO_ADMIN_PASSWORD")
admin_email = getattr(args, "admin_email", None) or os.environ.get("MATOMO_ADMIN_EMAIL")
admin_user = getattr(args, "admin_user", None) or os.environ.get(
"MATOMO_ADMIN_USER"
)
admin_password = getattr(args, "admin_password", None) or os.environ.get(
"MATOMO_ADMIN_PASSWORD"
)
admin_email = getattr(args, "admin_email", None) or os.environ.get(
"MATOMO_ADMIN_EMAIL"
)
token_description = (
getattr(args, "token_description", None)
@@ -31,7 +39,9 @@ def config_from_env_and_args(args) -> Config:
or "matomo-bootstrap"
)
timeout = int(getattr(args, "timeout", None) or os.environ.get("MATOMO_TIMEOUT") or "20")
timeout = int(
getattr(args, "timeout", None) or os.environ.get("MATOMO_TIMEOUT") or "20"
)
debug = bool(getattr(args, "debug", False))
matomo_container_name = (

View File

@@ -11,11 +11,15 @@ from ..config import Config
# Optional knobs (mostly for debugging / CI stability)
PLAYWRIGHT_HEADLESS = (
os.environ.get("MATOMO_PLAYWRIGHT_HEADLESS", "1").strip() not in ("0", "false", "False")
PLAYWRIGHT_HEADLESS = os.environ.get("MATOMO_PLAYWRIGHT_HEADLESS", "1").strip() not in (
"0",
"false",
"False",
)
PLAYWRIGHT_SLOWMO_MS = int(os.environ.get("MATOMO_PLAYWRIGHT_SLOWMO_MS", "0"))
PLAYWRIGHT_NAV_TIMEOUT_MS = int(os.environ.get("MATOMO_PLAYWRIGHT_NAV_TIMEOUT_MS", "60000"))
PLAYWRIGHT_NAV_TIMEOUT_MS = int(
os.environ.get("MATOMO_PLAYWRIGHT_NAV_TIMEOUT_MS", "60000")
)
# Values used by the installer flow (recorded)
DEFAULT_SITE_NAME = os.environ.get("MATOMO_SITE_NAME", "localhost")
@@ -49,10 +53,14 @@ def wait_http(url: str, timeout: int = 180) -> None:
except Exception as exc:
last_err = exc
if i % 5 == 0:
_log(f"[install] still waiting ({i}/{timeout}) … ({type(exc).__name__})")
_log(
f"[install] still waiting ({i}/{timeout}) … ({type(exc).__name__})"
)
time.sleep(1)
raise RuntimeError(f"Matomo did not become reachable after {timeout}s: {url} ({last_err})")
raise RuntimeError(
f"Matomo did not become reachable after {timeout}s: {url} ({last_err})"
)
def is_installed(url: str) -> bool:
@@ -64,11 +72,19 @@ def is_installed(url: str) -> bool:
try:
with urllib.request.urlopen(url, timeout=5) as resp:
html = resp.read().decode(errors="ignore").lower()
return ("module=login" in html) or ("matomo login" in html) or ("matomo/login" in html)
return (
("module=login" in html)
or ("matomo login" in html)
or ("matomo/login" in html)
)
except urllib.error.HTTPError as exc:
try:
html = exc.read().decode(errors="ignore").lower()
return ("module=login" in html) or ("matomo login" in html) or ("matomo/login" in html)
return (
("module=login" in html)
or ("matomo login" in html)
or ("matomo/login" in html)
)
except Exception:
return False
except Exception:
@@ -143,7 +159,9 @@ class WebInstaller(Installer):
loc.first.click()
return
raise RuntimeError("Could not find a Next/Continue control in the installer UI.")
raise RuntimeError(
"Could not find a Next/Continue control in the installer UI."
)
page.goto(base_url, wait_until="domcontentloaded")
@@ -157,7 +175,9 @@ class WebInstaller(Installer):
page.wait_for_load_state("domcontentloaded")
page.wait_for_timeout(200)
else:
raise RuntimeError("Installer did not reach superuser step (login-0 not found).")
raise RuntimeError(
"Installer did not reach superuser step (login-0 not found)."
)
page.locator("#login-0").click()
page.locator("#login-0").fill(config.admin_user)

View File

@@ -70,7 +70,10 @@ class MatomoApi:
err_body = exc.read().decode("utf-8", errors="replace")
except Exception:
err_body = ""
_dbg(f"[auth] logme HTTPError {exc.code} body[:120]={err_body[:120]!r}", self.debug)
_dbg(
f"[auth] logme HTTPError {exc.code} body[:120]={err_body[:120]!r}",
self.debug,
)
def create_app_specific_token(
self,
@@ -85,7 +88,9 @@ class MatomoApi:
"""
env_token = os.environ.get("MATOMO_BOOTSTRAP_TOKEN_AUTH")
if env_token:
_dbg("[auth] Using MATOMO_BOOTSTRAP_TOKEN_AUTH from environment.", self.debug)
_dbg(
"[auth] Using MATOMO_BOOTSTRAP_TOKEN_AUTH from environment.", self.debug
)
return env_token
self.login_via_logme(admin_user, admin_password)
@@ -102,10 +107,15 @@ class MatomoApi:
},
)
_dbg(f"[auth] createAppSpecificTokenAuth HTTP {status} body[:200]={body[:200]!r}", self.debug)
_dbg(
f"[auth] createAppSpecificTokenAuth HTTP {status} body[:200]={body[:200]!r}",
self.debug,
)
if status != 200:
raise TokenCreationError(f"HTTP {status} during token creation: {body[:400]}")
raise TokenCreationError(
f"HTTP {status} during token creation: {body[:400]}"
)
data = _try_json(body)
token = data.get("value") if isinstance(data, dict) else None