#!/usr/bin/env python3 from __future__ import annotations import os import sys from pathlib import Path from urllib.parse import urlparse PLACEHOLDER_VALUES = { "", "replace-with-a-random-32-byte-or-longer-secret", "your-forgejo-oauth-client-id", "your-forgejo-oauth-client-secret", } def main() -> int: root_dir = Path(__file__).resolve().parents[1] _load_env_file(root_dir / ".env") _load_env_file(root_dir / ".env.local") errors: list[str] = [] warnings: list[str] = [] app_base_url = _required_env("APP_BASE_URL", errors) parsed_app_url = urlparse(app_base_url) if app_base_url and parsed_app_url.scheme not in {"http", "https"}: errors.append("APP_BASE_URL must start with http:// or https://.") if parsed_app_url.scheme == "http": warnings.append("APP_BASE_URL uses http://. Use https:// for public deployment.") auth_secret = _required_env("AUTH_SECRET_KEY", errors) if auth_secret in PLACEHOLDER_VALUES or len(auth_secret) < 32: errors.append("AUTH_SECRET_KEY must be a real random secret at least 32 characters long.") auth_cookie_secure = _env_bool("AUTH_COOKIE_SECURE") if parsed_app_url.scheme == "https" and not auth_cookie_secure: errors.append("AUTH_COOKIE_SECURE=true is required when APP_BASE_URL uses https://.") if parsed_app_url.scheme == "http" and auth_cookie_secure: warnings.append("AUTH_COOKIE_SECURE=true will prevent cookies over plain HTTP.") _required_env("FORGEJO_BASE_URL", errors) _required_env("FORGEJO_OAUTH_CLIENT_ID", errors) _required_env("FORGEJO_OAUTH_CLIENT_SECRET", errors) general_repo = _required_env("FORGEJO_GENERAL_DISCUSSION_REPO", errors) if general_repo and len(general_repo.split("/", 1)) != 2: errors.append("FORGEJO_GENERAL_DISCUSSION_REPO must use owner/repo format.") cors_origins = _csv_env("CORS_ALLOW_ORIGINS") if not cors_origins: warnings.append("CORS_ALLOW_ORIGINS is not set. The app will default to APP_BASE_URL.") elif "*" in cors_origins: warnings.append("CORS_ALLOW_ORIGINS includes '*'. Avoid that for public deployment.") if not os.getenv("FORGEJO_WEBHOOK_SECRET"): warnings.append( "FORGEJO_WEBHOOK_SECRET is not set. Webhook cache invalidation is unauthenticated." ) _positive_number_env("FORGEJO_REPO_SCAN_LIMIT", errors) _positive_number_env("FORGEJO_RECENT_ISSUE_LIMIT", errors) _positive_number_env("CALENDAR_EVENT_LIMIT", errors) _non_negative_number_env("FORGEJO_CACHE_TTL_SECONDS", errors) _positive_number_env("FORGEJO_REQUEST_TIMEOUT_SECONDS", errors) for warning in warnings: print(f"WARNING: {warning}", file=sys.stderr) if errors: for error in errors: print(f"ERROR: {error}", file=sys.stderr) return 1 print("Deployment configuration looks usable.") return 0 def _load_env_file(path: Path) -> None: if not path.exists(): return for raw_line in path.read_text().splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key = key.strip().removeprefix("export ").strip() value = value.strip().strip('"').strip("'") os.environ.setdefault(key, value) def _required_env(name: str, errors: list[str]) -> str: value = os.getenv(name, "").strip() if value in PLACEHOLDER_VALUES: errors.append(f"{name} is required.") return value def _env_bool(name: str) -> bool: return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"} def _csv_env(name: str) -> tuple[str, ...]: value = os.getenv(name, "").strip() if not value: return () return tuple(entry.strip() for entry in value.replace("\n", ",").split(",") if entry.strip()) def _positive_number_env(name: str, errors: list[str]) -> None: value = os.getenv(name, "").strip() if not value: return try: parsed_value = float(value) except ValueError: errors.append(f"{name} must be numeric.") return if parsed_value <= 0: errors.append(f"{name} must be greater than zero.") def _non_negative_number_env(name: str, errors: list[str]) -> None: value = os.getenv(name, "").strip() if not value: return try: parsed_value = float(value) except ValueError: errors.append(f"{name} must be numeric.") return if parsed_value < 0: errors.append(f"{name} must be zero or greater.") if __name__ == "__main__": raise SystemExit(main())