robot-u-site/scripts/check_deploy_config.py
kacper 853e99ca5f
Some checks failed
CI / check (push) Failing after 8s
Prepare deployment and Forgejo CI
2026-04-14 20:17:29 -04:00

140 lines
4.6 KiB
Python
Executable file

#!/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())