Make Gitea publishing claimant controlled
This commit is contained in:
@@ -50,7 +50,7 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
def _get_headers(self, token: Optional[str] = None) -> Dict[str, str]:
|
||||
t = token or self.get_effective_token()
|
||||
headers = {
|
||||
"User-Agent": "ThinkStorm-Orchestrator/0.1",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
if t:
|
||||
@@ -62,13 +62,14 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
url = f"{self.endpoint}/api/v1/version"
|
||||
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
|
||||
def fetch():
|
||||
return requests.get(url, headers={"User-Agent": "ThinkStorm-Orchestrator/0.1"}, timeout=6.0)
|
||||
return requests.get(url, headers=headers, timeout=15.0)
|
||||
resp = await loop.run_in_executor(None, fetch)
|
||||
elapsed = int((time.time() - start) * 1000)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
version = data.get("version", "unknown")
|
||||
elapsed = int((time.time() - start) * 1000)
|
||||
return ServiceHealth(
|
||||
service_id=self.service_id,
|
||||
healthy=True,
|
||||
@@ -77,15 +78,13 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
response_time_ms=elapsed,
|
||||
extra={"version": version}
|
||||
)
|
||||
else:
|
||||
elapsed = int((time.time() - start) * 1000)
|
||||
return ServiceHealth(
|
||||
service_id=self.service_id,
|
||||
healthy=False,
|
||||
endpoint=self.endpoint,
|
||||
message=f"Gitea HTTP error: {resp.status_code}",
|
||||
response_time_ms=elapsed
|
||||
)
|
||||
return ServiceHealth(
|
||||
service_id=self.service_id,
|
||||
healthy=False,
|
||||
endpoint=self.endpoint,
|
||||
message=f"Gitea returned HTTP {resp.status_code}",
|
||||
response_time_ms=elapsed
|
||||
)
|
||||
except Exception as e:
|
||||
elapsed = int((time.time() - start) * 1000)
|
||||
return ServiceHealth(
|
||||
@@ -100,7 +99,7 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
"""Ensures the 'thinkstorm' organization exists."""
|
||||
headers = self._get_headers(token)
|
||||
try:
|
||||
r = session.get(f"{self.endpoint}/api/v1/orgs/{self.org_name}", headers=headers, timeout=8)
|
||||
r = session.get(f"{self.endpoint}/api/v1/orgs/{self.org_name}", headers=headers, timeout=60)
|
||||
if r.status_code == 200:
|
||||
return
|
||||
if r.status_code == 404:
|
||||
@@ -110,7 +109,7 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
"description": "Canonical dossiers, research, and project incubations generated by ThinkStorm",
|
||||
"visibility": "public"
|
||||
}
|
||||
session.post(f"{self.endpoint}/api/v1/orgs", json=payload, headers=headers, timeout=8)
|
||||
session.post(f"{self.endpoint}/api/v1/orgs", json=payload, headers=headers, timeout=60)
|
||||
except Exception as e:
|
||||
print(f"[Gitea] Org ensure notice: {e}")
|
||||
|
||||
@@ -118,7 +117,7 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
"""Ensures the repository exists under the organization."""
|
||||
headers = self._get_headers(token)
|
||||
try:
|
||||
r = session.get(f"{self.endpoint}/api/v1/repos/{self.org_name}/{repo_name}", headers=headers, timeout=8)
|
||||
r = session.get(f"{self.endpoint}/api/v1/repos/{self.org_name}/{repo_name}", headers=headers, timeout=60)
|
||||
if r.status_code == 200:
|
||||
return True
|
||||
if r.status_code == 404:
|
||||
@@ -129,13 +128,38 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
"auto_init": True,
|
||||
"default_branch": "main"
|
||||
}
|
||||
cr = session.post(f"{self.endpoint}/api/v1/orgs/{self.org_name}/repos", json=payload, headers=headers, timeout=10)
|
||||
cr = session.post(f"{self.endpoint}/api/v1/orgs/{self.org_name}/repos", json=payload, headers=headers, timeout=60)
|
||||
return cr.status_code in (200, 201)
|
||||
except Exception as e:
|
||||
print(f"[Gitea] Repo ensure error for {repo_name}: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
def _grant_repo_write_access(
|
||||
self,
|
||||
session: requests.Session,
|
||||
token: str,
|
||||
repo_name: str,
|
||||
username: str
|
||||
) -> None:
|
||||
"""Idempotently grants the verified claimant write access to a repository."""
|
||||
safe_username = requests.utils.quote(username, safe="")
|
||||
url = (
|
||||
f"{self.endpoint}/api/v1/repos/{self.org_name}/{repo_name}"
|
||||
f"/collaborators/{safe_username}"
|
||||
)
|
||||
response = session.put(
|
||||
url,
|
||||
json={"permission": "write"},
|
||||
headers=self._get_headers(token),
|
||||
timeout=60
|
||||
)
|
||||
if response.status_code not in (200, 201, 204):
|
||||
raise RuntimeError(
|
||||
f"Could not grant Gitea write access to '{username}' "
|
||||
f"(HTTP {response.status_code})."
|
||||
)
|
||||
|
||||
def _sync_files_via_api(self, token: str, repo_slug: str, files_dict: Dict[str, str]) -> None:
|
||||
"""Commits or updates multiple files directly into the Gitea repository via Contents API."""
|
||||
if not files_dict or not token:
|
||||
@@ -147,7 +171,7 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
try:
|
||||
# 1. Check if file already exists in repo to get current SHA
|
||||
get_url = f"{self.endpoint}/api/v1/repos/{self.org_name}/{repo_slug}/contents/{path}"
|
||||
r = s.get(get_url, headers=headers, timeout=8)
|
||||
r = s.get(get_url, headers=headers, timeout=60)
|
||||
sha = None
|
||||
if r.status_code == 200:
|
||||
sha = r.json().get("sha")
|
||||
@@ -161,11 +185,11 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
|
||||
if sha:
|
||||
payload["sha"] = sha
|
||||
put_res = s.put(get_url, json=payload, headers=headers, timeout=10)
|
||||
put_res = s.put(get_url, json=payload, headers=headers, timeout=60)
|
||||
if put_res.status_code not in (200, 201):
|
||||
print(f"[Gitea] Update notice for {path}: {put_res.status_code} {put_res.text[:100]}")
|
||||
else:
|
||||
post_res = s.post(get_url, json=payload, headers=headers, timeout=10)
|
||||
post_res = s.post(get_url, json=payload, headers=headers, timeout=60)
|
||||
if post_res.status_code not in (200, 201):
|
||||
print(f"[Gitea] Create notice for {path}: {post_res.status_code} {post_res.text[:100]}")
|
||||
except Exception as e:
|
||||
@@ -184,9 +208,11 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
outputs: Dict[str, str],
|
||||
provenance_runs: List[Dict[str, Any]],
|
||||
existing_repo_url: Optional[str] = None,
|
||||
submission_image: Optional[Dict[str, Any]] = None
|
||||
submission_image: Optional[Dict[str, Any]] = None,
|
||||
publish_remote: bool = True,
|
||||
collaborator_username: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Creates a dedicated Gitea Project repository and synchronizes all files into it."""
|
||||
"""Persists a dossier locally and optionally publishes it to Gitea."""
|
||||
# 1. Local disk directory
|
||||
idea_dir = ARTIFACTS_DIR / idea_id
|
||||
idea_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -284,43 +310,43 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
clone_url = f"{self.endpoint}/{self.org_name}/{repo_name}.git"
|
||||
ssh_url = f"ssh://[email protected]:22/{self.org_name}/{repo_name}.git"
|
||||
|
||||
token = self.get_effective_token()
|
||||
if token:
|
||||
token = self.get_effective_token() if publish_remote else ""
|
||||
if publish_remote:
|
||||
if not token:
|
||||
raise RuntimeError("Gitea publishing is not configured with a service token.")
|
||||
loop = asyncio.get_running_loop()
|
||||
def sync_gitea():
|
||||
try:
|
||||
with requests.Session() as s:
|
||||
# 1. Ensure org and repo exist
|
||||
self._ensure_org(s, token)
|
||||
self._ensure_repo(s, token, repo_name, f"[{idea_id}] {title}")
|
||||
# 2. Push full file tree to Gitea
|
||||
self._sync_files_via_api(token, repo_name, files_to_sync)
|
||||
return f"{self.endpoint}/{self.org_name}/{repo_name}"
|
||||
except Exception as e:
|
||||
print(f"[Gitea] Sync error: {e}")
|
||||
return repo_url
|
||||
with requests.Session() as s:
|
||||
self._ensure_org(s, token)
|
||||
if not self._ensure_repo(s, token, repo_name, f"[{idea_id}] {title}"):
|
||||
raise RuntimeError(f"Could not create or access Gitea repository '{repo_name}'.")
|
||||
if collaborator_username:
|
||||
self._grant_repo_write_access(
|
||||
s, token, repo_name, collaborator_username
|
||||
)
|
||||
self._sync_files_via_api(token, repo_name, files_to_sync)
|
||||
return f"{self.endpoint}/{self.org_name}/{repo_name}"
|
||||
|
||||
try:
|
||||
actual_url = await loop.run_in_executor(None, sync_gitea)
|
||||
repo_url = actual_url or repo_url
|
||||
except Exception as e:
|
||||
print(f"[Gitea] Async sync exception: {e}")
|
||||
actual_url = await loop.run_in_executor(None, sync_gitea)
|
||||
repo_url = actual_url or repo_url
|
||||
|
||||
return {
|
||||
"repo_name": f"{self.org_name}/{repo_name}",
|
||||
"repo_url": repo_url,
|
||||
"clone_url": clone_url,
|
||||
"ssh_url": ssh_url,
|
||||
"local_path": str(idea_dir)
|
||||
"local_path": str(idea_dir),
|
||||
"published": publish_remote
|
||||
}
|
||||
|
||||
async def persist_work_track_outputs(
|
||||
self,
|
||||
idea_id: str,
|
||||
track_name: str,
|
||||
outputs: Dict[str, str]
|
||||
outputs: Dict[str, str],
|
||||
publish_remote: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""Persists Work Track deliverables to local dossier and commits to Gitea project."""
|
||||
"""Persists Work Track deliverables locally and optionally pushes them."""
|
||||
idea_dir = ARTIFACTS_DIR / idea_id
|
||||
out_subdir = idea_dir / "outputs" / track_name.lower().replace(" ", "-")
|
||||
out_subdir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -335,7 +361,7 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
repo_name = f"ts-{clean_slug.replace('ts-', '')}"
|
||||
repo_url = f"{self.endpoint}/{self.org_name}/{repo_name}"
|
||||
|
||||
token = self.get_effective_token()
|
||||
token = self.get_effective_token() if publish_remote else ""
|
||||
if token and files_to_sync:
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
@@ -348,31 +374,40 @@ class GiteaAdapter(BaseServiceAdapter):
|
||||
print(f"[Gitea] Work track sync notice: {e}")
|
||||
|
||||
return {
|
||||
"status": "SUCCESS",
|
||||
"status": "SUCCESS" if publish_remote else "LOCAL_ONLY",
|
||||
"path": str(out_subdir),
|
||||
"repo_url": repo_url
|
||||
"repo_url": repo_url if publish_remote else ""
|
||||
}
|
||||
|
||||
async def graduate_project(self, idea_id: str, title: str, summary: str, repo_name: str = "") -> Dict[str, Any]:
|
||||
async def graduate_project(
|
||||
self,
|
||||
idea_id: str,
|
||||
title: str,
|
||||
summary: str,
|
||||
repo_name: str = "",
|
||||
collaborator_username: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Graduates an incubated Coding Project work track to a dedicated repository under 'thinkstorm'."""
|
||||
clean_title = re.sub(r'[^a-zA-Z0-9_-]', '-', title.lower()).strip('-')[:30] or "project"
|
||||
slug = repo_name or f"ts-{idea_id.lower()}-{clean_title}"
|
||||
repo_url = f"{self.endpoint}/{self.org_name}/{slug}"
|
||||
|
||||
token = self.get_effective_token()
|
||||
if token:
|
||||
loop = asyncio.get_running_loop()
|
||||
def create_remote():
|
||||
try:
|
||||
with requests.Session() as s:
|
||||
self._ensure_org(s, token)
|
||||
self._ensure_repo(s, token, slug, f"[{idea_id}] {title}")
|
||||
return f"{self.endpoint}/{self.org_name}/{slug}"
|
||||
except Exception as e:
|
||||
print(f"[Gitea] Project graduation notice: {e}")
|
||||
return repo_url
|
||||
actual_url = await loop.run_in_executor(None, create_remote)
|
||||
repo_url = actual_url or repo_url
|
||||
if not token:
|
||||
raise RuntimeError("Gitea publishing is not configured with a service token.")
|
||||
loop = asyncio.get_running_loop()
|
||||
def create_remote():
|
||||
with requests.Session() as s:
|
||||
self._ensure_org(s, token)
|
||||
if not self._ensure_repo(s, token, slug, f"[{idea_id}] {title}"):
|
||||
raise RuntimeError(f"Could not create or access Gitea repository '{slug}'.")
|
||||
if collaborator_username:
|
||||
self._grant_repo_write_access(
|
||||
s, token, slug, collaborator_username
|
||||
)
|
||||
return f"{self.endpoint}/{self.org_name}/{slug}"
|
||||
actual_url = await loop.run_in_executor(None, create_remote)
|
||||
repo_url = actual_url or repo_url
|
||||
|
||||
return {
|
||||
"repo_name": f"{self.org_name}/{slug}",
|
||||
|
||||
Reference in New Issue
Block a user