case-create: surface Gitea repo result + UI retry button
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
The auto-creation in case_create had two failure modes that combined to
make repos silently missing: a stale GITEA_TOKEN returning 401, and the
outer try/except in case_create that swallowed every exception with a
bare pass. Result: cases like 8174-24 ended up with a local git repo and
Paperclip project but no Gitea repo, with no signal anywhere.
_setup_gitea_remote now returns {ok, url, error} and never raises; the
result is attached to the case JSON and the FastAPI endpoint logs a
warning when ok=false. The UI gets a "צור ריפו ב-Gitea" button on the
case header that appears only when the repo or remote is missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,12 +28,17 @@ def _gitea_token() -> str:
|
|||||||
return os.environ.get("GITEA_ACCESS_TOKEN") or os.environ.get("GITEA_TOKEN", "")
|
return os.environ.get("GITEA_ACCESS_TOKEN") or os.environ.get("GITEA_TOKEN", "")
|
||||||
|
|
||||||
|
|
||||||
async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> bool:
|
async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> dict:
|
||||||
"""Create Gitea repo and configure git remote. Best-effort — returns False on failure."""
|
"""Create Gitea repo and configure git remote.
|
||||||
|
|
||||||
|
Returns a dict with: ok (bool), url (str|None), error (str|None).
|
||||||
|
Never raises — failures are reported via the dict so callers can surface
|
||||||
|
them to the UI instead of silently swallowing them.
|
||||||
|
"""
|
||||||
token = _gitea_token()
|
token = _gitea_token()
|
||||||
if not token:
|
if not token:
|
||||||
logger.info("No GITEA_TOKEN — skipping Gitea repo creation for %s", case_number)
|
logger.info("No GITEA_TOKEN — skipping Gitea repo creation for %s", case_number)
|
||||||
return False
|
return {"ok": False, "url": None, "error": "no_token"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(verify=False, timeout=30) as client:
|
async with httpx.AsyncClient(verify=False, timeout=30) as client:
|
||||||
@@ -59,8 +64,9 @@ async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> b
|
|||||||
repo = resp.json()
|
repo = resp.json()
|
||||||
|
|
||||||
clone_url = repo.get("clone_url", "")
|
clone_url = repo.get("clone_url", "")
|
||||||
|
html_url = repo.get("html_url", "")
|
||||||
if not clone_url:
|
if not clone_url:
|
||||||
return False
|
return {"ok": False, "url": None, "error": "no_clone_url"}
|
||||||
|
|
||||||
auth_url = clone_url.replace("https://", f"https://chaim:{token}@")
|
auth_url = clone_url.replace("https://", f"https://chaim:{token}@")
|
||||||
|
|
||||||
@@ -94,15 +100,20 @@ async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> b
|
|||||||
cwd=case_dir, capture_output=True, text=True, env=git_env,
|
cwd=case_dir, capture_output=True, text=True, env=git_env,
|
||||||
)
|
)
|
||||||
if push.returncode != 0:
|
if push.returncode != 0:
|
||||||
logger.warning("Gitea push failed for %s: %s", case_number, push.stderr)
|
stderr = push.stderr.strip()
|
||||||
return False
|
logger.warning("Gitea push failed for %s: %s", case_number, stderr)
|
||||||
|
return {"ok": False, "url": html_url or None, "error": f"push_failed: {stderr[:200]}"}
|
||||||
|
|
||||||
logger.info("Gitea repo created and pushed for %s", case_number)
|
logger.info("Gitea repo created and pushed for %s", case_number)
|
||||||
return True
|
return {"ok": True, "url": html_url or None, "error": None}
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
msg = f"http_{exc.response.status_code}"
|
||||||
|
logger.warning("Gitea setup failed for %s: %s", case_number, msg)
|
||||||
|
return {"ok": False, "url": None, "error": msg}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Gitea setup failed for %s: %s", case_number, exc)
|
logger.warning("Gitea setup failed for %s: %s", case_number, exc)
|
||||||
return False
|
return {"ok": False, "url": None, "error": f"{type(exc).__name__}: {exc}"[:200]}
|
||||||
|
|
||||||
|
|
||||||
async def case_create(
|
async def case_create(
|
||||||
@@ -214,11 +225,10 @@ async def case_create(
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass # git not available — non-critical
|
pass # git not available — non-critical
|
||||||
|
|
||||||
# Create Gitea repo and configure remote (best-effort)
|
# Create Gitea repo and configure remote — surface result so callers can
|
||||||
try:
|
# show failures (e.g. stale token) and offer a retry button instead of
|
||||||
await _setup_gitea_remote(case_number, title, case_dir)
|
# silently producing a case with no remote.
|
||||||
except Exception:
|
case["gitea"] = await _setup_gitea_remote(case_number, title, case_dir)
|
||||||
pass # Gitea not available — non-critical
|
|
||||||
|
|
||||||
return json.dumps(case, default=str, ensure_ascii=False, indent=2)
|
return json.dumps(case, default=str, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { StatusBadge } from "@/components/cases/status-badge";
|
import { StatusBadge } from "@/components/cases/status-badge";
|
||||||
import { SyncIndicator } from "@/components/cases/sync-indicator";
|
import { SyncIndicator } from "@/components/cases/sync-indicator";
|
||||||
import { CaseArchiveAction } from "@/components/cases/case-archive-action";
|
import { CaseArchiveAction } from "@/components/cases/case-archive-action";
|
||||||
|
import { CreateRepoButton } from "@/components/cases/create-repo-button";
|
||||||
import {
|
import {
|
||||||
PRACTICE_AREA_LABELS,
|
PRACTICE_AREA_LABELS,
|
||||||
APPEAL_SUBTYPE_LABELS,
|
APPEAL_SUBTYPE_LABELS,
|
||||||
@@ -67,6 +68,7 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
|
|||||||
archivedAt={data.archived_at}
|
archivedAt={data.archived_at}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<CreateRepoButton data={data} />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-navy text-xl font-bold leading-snug max-w-2xl mb-0">
|
<h1 className="text-navy text-xl font-bold leading-snug max-w-2xl mb-0">
|
||||||
{data?.title ?? "טוען…"}
|
{data?.title ?? "טוען…"}
|
||||||
|
|||||||
59
web-ui/src/components/cases/create-repo-button.tsx
Normal file
59
web-ui/src/components/cases/create-repo-button.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Cloud, Loader2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
useCreateGiteaRepo,
|
||||||
|
useGitStatus,
|
||||||
|
type CaseDetail,
|
||||||
|
} from "@/lib/api/cases";
|
||||||
|
|
||||||
|
export function CreateRepoButton({ data }: { data?: CaseDetail }) {
|
||||||
|
const { data: gitStatus } = useGitStatus(data?.case_number);
|
||||||
|
const createRepo = useCreateGiteaRepo(data?.case_number);
|
||||||
|
|
||||||
|
if (!data?.case_number || !gitStatus) return null;
|
||||||
|
|
||||||
|
/* Show only when something is actually missing — repo, remote, or
|
||||||
|
* unpushed commits. If everything is in sync, the SyncIndicator already
|
||||||
|
* communicates that. */
|
||||||
|
const needsRepo = gitStatus.error === "no_repo" || !gitStatus.has_remote;
|
||||||
|
if (!needsRepo) return null;
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (!data) return;
|
||||||
|
createRepo.mutate(
|
||||||
|
{ title: data.title ?? "", description: data.subject ?? "" },
|
||||||
|
{
|
||||||
|
onSuccess: (res) =>
|
||||||
|
toast.success(
|
||||||
|
res.pushed
|
||||||
|
? `הריפו נוצר ב-Gitea: ${res.repo_url}`
|
||||||
|
: `הריפו נוצר אך ה-push נכשל. בדוק את הלוגים של ה-backend.`,
|
||||||
|
),
|
||||||
|
onError: (err) =>
|
||||||
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : "יצירת הריפו נכשלה",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={createRepo.isPending}
|
||||||
|
title="יצירת ריפו ב-Gitea וקישור התיק המקומי אליו"
|
||||||
|
>
|
||||||
|
{createRepo.isPending ? (
|
||||||
|
<Loader2 className="me-1 h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Cloud className="me-1 h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
צור ריפו ב-Gitea
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -217,6 +217,30 @@ export function useGitStatus(caseNumber: string | undefined) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateGiteaRepoResult = {
|
||||||
|
repo_url: string;
|
||||||
|
clone_url: string;
|
||||||
|
pushed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useCreateGiteaRepo(caseNumber: string | undefined) {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (input: { title: string; description?: string }) =>
|
||||||
|
apiRequest<CreateGiteaRepoResult>(`/api/integrations/gitea/create-repo`, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
case_number: caseNumber,
|
||||||
|
title: input.title,
|
||||||
|
description: input.description ?? "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: [...casesKeys.all, "git-status", caseNumber ?? ""] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export type StartWorkflowResult = {
|
export type StartWorkflowResult = {
|
||||||
case_number: string;
|
case_number: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@@ -1214,6 +1214,15 @@ async def api_case_create(req: CaseCreateRequest):
|
|||||||
logger.warning("Failed to auto-create Paperclip project for case %s: %s", req.case_number, e)
|
logger.warning("Failed to auto-create Paperclip project for case %s: %s", req.case_number, e)
|
||||||
parsed["paperclip_error"] = str(e)
|
parsed["paperclip_error"] = str(e)
|
||||||
|
|
||||||
|
# Gitea result was attached by case_create itself; log if it failed so
|
||||||
|
# ops can spot stale-token issues without scanning every response.
|
||||||
|
gitea_result = parsed.get("gitea") or {}
|
||||||
|
if not gitea_result.get("ok"):
|
||||||
|
logger.warning(
|
||||||
|
"Gitea repo not created for case %s: %s",
|
||||||
|
req.case_number, gitea_result.get("error") or "unknown",
|
||||||
|
)
|
||||||
|
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user