scheduled commits + devs info
This commit is contained in:
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends
|
|||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
from ..auth import require_user
|
from ..auth import require_user
|
||||||
|
from ..config import DEFAULT_GITEA_ORG, get_gitea_public_url, read_env_value
|
||||||
from ..templates.layout import page
|
from ..templates.layout import page
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -86,6 +87,11 @@ def developers_page(user=Depends(require_user)):
|
|||||||
</tr>
|
</tr>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
gitea_url = get_gitea_public_url()
|
||||||
|
gitea_org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG)
|
||||||
|
clone_base = gitea_url or "<gitea-url>"
|
||||||
|
clone_cmd = html.escape(f"git clone {clone_base}/{gitea_org}/<app-id>.git", quote=True)
|
||||||
|
|
||||||
return page(
|
return page(
|
||||||
"Pro vývojáře",
|
"Pro vývojáře",
|
||||||
f"""
|
f"""
|
||||||
@@ -97,6 +103,42 @@ def developers_page(user=Depends(require_user)):
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2><i class="fa-solid fa-cloud-arrow-up" aria-hidden="true"></i> Jak publikovat</h2>
|
||||||
|
<p class="muted">Postup od založení nové služby po nasazení změn.</p>
|
||||||
|
<ol class="publish-steps">
|
||||||
|
<li>
|
||||||
|
<strong>Vytvořte novou službu.</strong>
|
||||||
|
Otevřete <a href="/portal/new-app" target="_blank" rel="noopener">formulář Nová služba</a>;
|
||||||
|
vznikne Gitea repozitář, workspace a první nasazení.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Naklonujte repozitář.</strong>
|
||||||
|
<div class="cmd-row">
|
||||||
|
<input readonly value="{clone_cmd}" id="publish-clone">
|
||||||
|
<button type="button" onclick="return copyText(this)">Kopírovat</button>
|
||||||
|
</div>
|
||||||
|
<span class="muted">Přesný příkaz pro konkrétní službu (HTTP i SSH) najdete také na stránce
|
||||||
|
<a href="/portal/apps">Služby</a>.</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Upravte kód.</strong>
|
||||||
|
Buď se řiďte souborem <code>AGENTS.md</code> v kořenové složce projektu, nebo úpravy
|
||||||
|
nechte provést AI (agent si <code>AGENTS.md</code> přečte sám).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Commit a push.</strong>
|
||||||
|
Zacommitujte a pushněte změny — build a nasazení se spustí automaticky
|
||||||
|
(worker je vždy spuštěný a změnu rovnou zpracuje).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Zkontrolujte výsledek.</strong>
|
||||||
|
Stav nasazení a běh služby ověříte na stránce
|
||||||
|
<a href="/portal/apps">Služby</a> (stav, health, dokumentace).
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2><i class="fa-solid fa-diagram-project" aria-hidden="true"></i> Sekce portálu (agendy)</h2>
|
<h2><i class="fa-solid fa-diagram-project" aria-hidden="true"></i> Sekce portálu (agendy)</h2>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import html
|
import html
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
from app.auth import require_user
|
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.audit import log_audit_event
|
||||||
from app.db.jobs import create_job
|
from app.db.jobs import create_job
|
||||||
from app.db.scheduled_scripts import (
|
from app.db.scheduled_scripts import (
|
||||||
@@ -20,7 +27,11 @@ from app.routes.deployments import render_status_pill
|
|||||||
from app.templates.layout import page
|
from app.templates.layout import page
|
||||||
|
|
||||||
router = APIRouter()
|
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
|
MAX_SCRIPT_BYTES = 100 * 1024
|
||||||
DEFAULT_SCRIPT_CONTENT = """#!/usr/bin/env bash
|
DEFAULT_SCRIPT_CONTENT = """#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
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")
|
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:
|
def validate_schedule_type(value: str) -> str:
|
||||||
schedule_type = clean_optional(value)
|
schedule_type = clean_optional(value)
|
||||||
if schedule_type not in SCHEDULE_TYPES:
|
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 = form_metadata(name, description, schedule_type, schedule_time, timeout_seconds, is_enabled)
|
||||||
metadata["script_name"] = validate_script_name(script_name)
|
metadata["script_name"] = validate_script_name(script_name)
|
||||||
script_id = create_scheduled_script(metadata)
|
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(
|
log_audit_event(
|
||||||
user,
|
user,
|
||||||
action="scheduled_script.created",
|
action="scheduled_script.created",
|
||||||
@@ -557,6 +631,7 @@ def update_scheduled_script_file(
|
|||||||
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||||
script_name = validate_script_name(script.get("script_name", "") or "")
|
script_name = validate_script_name(script.get("script_name", "") or "")
|
||||||
save_script_file(script_name, content)
|
save_script_file(script_name, content)
|
||||||
|
commit_and_push(script_name, f"Úprava skriptu {script_name}", user)
|
||||||
log_audit_event(
|
log_audit_event(
|
||||||
user,
|
user,
|
||||||
action="scheduled_script.file_updated",
|
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")
|
raise HTTPException(status_code=409, detail="Běžící skript nelze smazat")
|
||||||
if not delete_scheduled_script(script_id):
|
if not delete_scheduled_script(script_id):
|
||||||
raise HTTPException(status_code=409, detail="Skript nelze smazat")
|
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(
|
log_audit_event(
|
||||||
user,
|
user,
|
||||||
action="scheduled_script.deleted",
|
action="scheduled_script.deleted",
|
||||||
|
|||||||
Reference in New Issue
Block a user