This commit is contained in:
parent
51706d2d11
commit
853e99ca5f
21 changed files with 1402 additions and 77 deletions
140
scripts/check_deploy_config.py
Executable file
140
scripts/check_deploy_config.py
Executable file
|
|
@ -0,0 +1,140 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue