From be2e71c7d03f7364d73d33501a6a6eb9afcff4db Mon Sep 17 00:00:00 2001
From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com>
Date: Mon, 15 Jun 2026 11:50:33 +0200
Subject: [PATCH] scheduled commits + devs info
---
app/routes/developers.py | 42 +++++++++++++++++
app/routes/scheduled_scripts.py | 83 ++++++++++++++++++++++++++++++++-
2 files changed, 124 insertions(+), 1 deletion(-)
diff --git a/app/routes/developers.py b/app/routes/developers.py
index aef4b48..19512c9 100644
--- a/app/routes/developers.py
+++ b/app/routes/developers.py
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends
from fastapi.responses import HTMLResponse
from ..auth import require_user
+from ..config import DEFAULT_GITEA_ORG, get_gitea_public_url, read_env_value
from ..templates.layout import page
router = APIRouter()
@@ -86,6 +87,11 @@ def developers_page(user=Depends(require_user)):
"""
+ gitea_url = get_gitea_public_url()
+ gitea_org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG)
+ clone_base = gitea_url or ""
+ clone_cmd = html.escape(f"git clone {clone_base}/{gitea_org}/.git", quote=True)
+
return page(
"Pro vývojáře",
f"""
@@ -97,6 +103,42 @@ def developers_page(user=Depends(require_user)):
+
+
Jak publikovat
+
Postup od založení nové služby po nasazení změn.
+
+ -
+ Vytvořte novou službu.
+ Otevřete formulář Nová služba;
+ vznikne Gitea repozitář, workspace a první nasazení.
+
+ -
+ Naklonujte repozitář.
+
+
+
+
+ Přesný příkaz pro konkrétní službu (HTTP i SSH) najdete také na stránce
+ Služby.
+
+ -
+ Upravte kód.
+ Buď se řiďte souborem
AGENTS.md v kořenové složce projektu, nebo úpravy
+ nechte provést AI (agent si AGENTS.md přečte sám).
+
+ -
+ Commit a push.
+ Zacommitujte a pushněte změny — build a nasazení se spustí automaticky
+ (worker je vždy spuštěný a změnu rovnou zpracuje).
+
+ -
+ Zkontrolujte výsledek.
+ Stav nasazení a běh služby ověříte na stránce
+ Služby (stav, health, dokumentace).
+
+
+
+
Sekce portálu (agendy)
diff --git a/app/routes/scheduled_scripts.py b/app/routes/scheduled_scripts.py
index 3026a69..863746f 100644
--- a/app/routes/scheduled_scripts.py
+++ b/app/routes/scheduled_scripts.py
@@ -1,11 +1,18 @@
import html
import os
+import subprocess
from pathlib import Path
from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from app.auth import require_user
+from app.config import (
+ DEFAULT_GITEA_ORG,
+ get_gitea_admin_token,
+ get_gitea_server_url,
+ read_env_value,
+)
from app.db.audit import log_audit_event
from app.db.jobs import create_job
from app.db.scheduled_scripts import (
@@ -20,7 +27,11 @@ from app.routes.deployments import render_status_pill
from app.templates.layout import page
router = APIRouter()
-MAINTENANCE_DIR = Path("/opt/appfactory/workspace/appfactory-tools/maintenance")
+TOOLS_REPO_DIR = Path("/opt/appfactory/workspace/appfactory-tools")
+TOOLS_REPO_NAME = "appfactory-tools"
+MAINTENANCE_DIR = TOOLS_REPO_DIR / "maintenance"
+GIT_AUTHOR_NAME = "AppFactory Portal"
+GIT_AUTHOR_EMAIL = "portal@appfactory.local"
MAX_SCRIPT_BYTES = 100 * 1024
DEFAULT_SCRIPT_CONTENT = """#!/usr/bin/env bash
set -euo pipefail
@@ -142,6 +153,65 @@ def save_script_file(script_name: str, content: str) -> None:
raise HTTPException(status_code=500, detail="Soubor se nepodařilo uložit")
+def _git(args: list[str]):
+ env = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
+ try:
+ return subprocess.run(
+ ["git", "-C", str(TOOLS_REPO_DIR), *args],
+ capture_output=True,
+ text=True,
+ env=env,
+ timeout=30,
+ )
+ except (subprocess.TimeoutExpired, OSError):
+ raise HTTPException(status_code=500, detail="Git příkaz selhal nebo vypršel limit")
+
+
+def _git_actor(user: dict) -> str:
+ return (user.get("email") or user.get("display_name") or user.get("username") or "neznámý").strip()
+
+
+def _gitea_push_remote() -> tuple[str, str | None]:
+ """Vrátí (remote, token). Remote je autentizovaná gitea URL z tokenů v proměnných,
+ při absenci tokenu fallback na origin. Token vracíme zvlášť, aby šel zamaskovat v chybách."""
+ token = get_gitea_admin_token()
+ server = get_gitea_server_url()
+ if not token or "://" not in server:
+ return "origin", None
+ scheme, rest = server.split("://", 1)
+ org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG)
+ return f"{scheme}://{token}@{rest}/{org}/{TOOLS_REPO_NAME}.git", token
+
+
+def commit_and_push(script_name: str, message: str, user: dict) -> None:
+ """Zacommituje a pushne změnu jednoho maintenance skriptu do gitea (appfactory-tools)."""
+ rel_path = f"maintenance/{script_name}"
+
+ if _git(["add", "--", rel_path]).returncode != 0:
+ raise HTTPException(status_code=500, detail="Git: soubor se nepodařilo přidat do indexu")
+
+ # Žádná změna oproti HEAD -> přeskočíme, ať nevznikají prázdné commity.
+ if _git(["diff", "--cached", "--quiet", "--", rel_path]).returncode == 0:
+ return
+
+ commit = _git([
+ "-c", f"user.name={GIT_AUTHOR_NAME}",
+ "-c", f"user.email={GIT_AUTHOR_EMAIL}",
+ "commit", "-m", f"{message} (portál: {_git_actor(user)})",
+ ])
+ if commit.returncode != 0:
+ raise HTTPException(status_code=500, detail="Git commit selhal")
+
+ branch = (_git(["rev-parse", "--abbrev-ref", "HEAD"]).stdout or "").strip() or "main"
+ remote, token = _gitea_push_remote()
+ push = _git(["push", remote, f"HEAD:{branch}"])
+ if push.returncode != 0:
+ detail = (push.stderr or "").strip()
+ if token:
+ detail = detail.replace(token, "***")
+ raise HTTPException(status_code=500, detail=f"Git push selhal: {html.escape(detail)}")
+
+
def validate_schedule_type(value: str) -> str:
schedule_type = clean_optional(value)
if schedule_type not in SCHEDULE_TYPES:
@@ -393,6 +463,10 @@ def create_scheduled_script_action(
metadata = form_metadata(name, description, schedule_type, schedule_time, timeout_seconds, is_enabled)
metadata["script_name"] = validate_script_name(script_name)
script_id = create_scheduled_script(metadata)
+ created_name = metadata["script_name"]
+ if not script_path(created_name).exists():
+ save_script_file(created_name, DEFAULT_SCRIPT_CONTENT)
+ commit_and_push(created_name, f"Vytvořen skript {created_name}", user)
log_audit_event(
user,
action="scheduled_script.created",
@@ -557,6 +631,7 @@ def update_scheduled_script_file(
raise HTTPException(status_code=404, detail="Scheduled script not found")
script_name = validate_script_name(script.get("script_name", "") or "")
save_script_file(script_name, content)
+ commit_and_push(script_name, f"Úprava skriptu {script_name}", user)
log_audit_event(
user,
action="scheduled_script.file_updated",
@@ -596,6 +671,12 @@ def delete_scheduled_script_action(script_id: int, user=Depends(require_user)):
raise HTTPException(status_code=409, detail="Běžící skript nelze smazat")
if not delete_scheduled_script(script_id):
raise HTTPException(status_code=409, detail="Skript nelze smazat")
+ script_name = clean_optional(script.get("script_name"))
+ if script_name:
+ path = script_path(script_name)
+ if path.exists():
+ path.unlink()
+ commit_and_push(script_name, f"Smazán skript {script_name}", user)
log_audit_event(
user,
action="scheduled_script.deleted",