Refine runtime git delivery scaffolding
This commit is contained in:
@@ -3,9 +3,14 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -60,6 +65,7 @@ class SwarmOrchestrator:
|
||||
self.callback: Optional[CallbackDeliveryClient] = None
|
||||
self.correlation_id: Optional[str] = None
|
||||
self.k8s_manager: Optional[K8sManager] = None
|
||||
self._git_workspace_dir: Optional[Path] = None
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""
|
||||
@@ -201,6 +207,7 @@ class SwarmOrchestrator:
|
||||
result = await self._execute_hybrid()
|
||||
|
||||
artifacts = self._ensure_result_artifacts(result)
|
||||
artifacts = self._attach_git_delivery_refs(artifacts)
|
||||
|
||||
# Update swarm status
|
||||
self.swarm.status = SwarmStatus.COMPLETED
|
||||
@@ -700,6 +707,11 @@ class SwarmOrchestrator:
|
||||
for client in self.agents.values():
|
||||
await client.close()
|
||||
self.agents.clear()
|
||||
if self._git_workspace_dir:
|
||||
workspace_root = self._git_workspace_dir.parent
|
||||
if workspace_root.exists():
|
||||
shutil.rmtree(workspace_root, ignore_errors=True)
|
||||
self._git_workspace_dir = None
|
||||
|
||||
def _callback_context(self) -> Dict[str, Any]:
|
||||
"""Return callback context stored on the swarm."""
|
||||
@@ -1288,6 +1300,188 @@ class SwarmOrchestrator:
|
||||
logger.warning("Failed to persist runtime project artifact %s: %s", artifact_id, e)
|
||||
return None
|
||||
|
||||
def _git_context(self) -> Optional[Dict[str, Any]]:
|
||||
"""Return repository context when Runtime should write delivery branches."""
|
||||
context = self._callback_context()
|
||||
repo_url = context.get("repo_url")
|
||||
if not repo_url:
|
||||
return None
|
||||
return {
|
||||
"repo_url": repo_url,
|
||||
"base_branch": context.get("branch") or "main",
|
||||
"git_binding_id": context.get("git_binding_id"),
|
||||
}
|
||||
|
||||
def _git_credentials(self) -> Optional[tuple[str, str]]:
|
||||
"""Resolve git credentials from runtime environment."""
|
||||
username = os.getenv("GITEE_USERNAME") or ""
|
||||
token = os.getenv("GITEE_TOKEN") or ""
|
||||
password = os.getenv("GITEE_PASSWORD") or ""
|
||||
if token and token != "your-gitee-token":
|
||||
return username or "oauth2", token
|
||||
if password and password != "your-gitee-password":
|
||||
return username or "git", password
|
||||
return None
|
||||
|
||||
def _inject_git_credentials(self, repo_url: str, username: str, password: str) -> str:
|
||||
"""Inject credentials into an HTTP(S) git URL without persisting them."""
|
||||
if "://" not in repo_url:
|
||||
return repo_url
|
||||
proto, rest = repo_url.split("://", 1)
|
||||
if "@" in rest:
|
||||
rest = rest.split("@", 1)[1]
|
||||
return f"{proto}://{username}:{password}@{rest}"
|
||||
|
||||
def _run_git(self, args: List[str], cwd: Path, *, timeout: int = 120) -> subprocess.CompletedProcess:
|
||||
"""Run a git command and raise on failure."""
|
||||
result = subprocess.run(
|
||||
args,
|
||||
cwd=str(cwd),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"git command failed: {' '.join(args)}")
|
||||
return result
|
||||
|
||||
def _ensure_git_workspace(self) -> Optional[Path]:
|
||||
"""Clone the target repository once and reuse it for role and delivery branches."""
|
||||
git_context = self._git_context()
|
||||
credentials = self._git_credentials()
|
||||
if not git_context or not credentials:
|
||||
return None
|
||||
if self._git_workspace_dir and self._git_workspace_dir.exists():
|
||||
return self._git_workspace_dir
|
||||
|
||||
username, password = credentials
|
||||
repo_url = git_context["repo_url"]
|
||||
auth_url = self._inject_git_credentials(repo_url, username, password)
|
||||
workspace = Path(tempfile.mkdtemp(prefix=f"swarm_git_{self.swarm_id}_"))
|
||||
repo_dir = workspace / "repo"
|
||||
self._run_git(["git", "clone", "--branch", git_context["base_branch"], auth_url, str(repo_dir)], workspace, timeout=180)
|
||||
self._run_git(["git", "remote", "set-url", "origin", repo_url], repo_dir)
|
||||
self._run_git(["git", "config", "user.name", "heicode-agent"], repo_dir)
|
||||
self._run_git(["git", "config", "user.email", "bot@heicode.local"], repo_dir)
|
||||
self._git_workspace_dir = repo_dir
|
||||
return repo_dir
|
||||
|
||||
def _write_project_files_to_repo(self, repo_dir: Path, files: Dict[str, str]) -> None:
|
||||
"""Materialize project artifact files into a git workspace."""
|
||||
for relative_path, content in files.items():
|
||||
file_path = repo_dir / relative_path
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content, encoding="utf-8")
|
||||
|
||||
def _git_repo_files_from_artifact(self, artifact: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Read back stored project artifact files for git delivery."""
|
||||
artifact_id = artifact.get("artifact_id")
|
||||
project = store = None
|
||||
from .artifact_store import load_runtime_project_artifact
|
||||
project = load_runtime_project_artifact(self.swarm_id, artifact_id)
|
||||
if not project:
|
||||
return {}
|
||||
files = {}
|
||||
root_dir = project.artifact_dir / project.root_dir
|
||||
for file_path in sorted(root_dir.rglob("*")):
|
||||
if file_path.is_file():
|
||||
rel = file_path.relative_to(root_dir).as_posix()
|
||||
files[rel] = file_path.read_text(encoding="utf-8", errors="replace")
|
||||
return files
|
||||
|
||||
def _attach_git_delivery_refs(self, artifacts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Best-effort git branch/commit materialization for project-folder artifacts."""
|
||||
repo_dir = self._ensure_git_workspace()
|
||||
if not repo_dir:
|
||||
return artifacts
|
||||
|
||||
git_context = self._git_context() or {}
|
||||
credentials = self._git_credentials()
|
||||
if not credentials:
|
||||
return artifacts
|
||||
username, password = credentials
|
||||
origin_url = self._run_git(["git", "remote", "get-url", "origin"], repo_dir).stdout.strip()
|
||||
auth_url = self._inject_git_credentials(origin_url, username, password)
|
||||
base_branch = git_context["base_branch"]
|
||||
|
||||
role_branches = []
|
||||
updated_artifacts = []
|
||||
base_git_ref = self._git_ref_metadata() or {}
|
||||
for artifact in artifacts:
|
||||
metadata = artifact.get("metadata") or {}
|
||||
if metadata.get("artifact_layout") != "project_folder":
|
||||
updated_artifacts.append(artifact)
|
||||
continue
|
||||
|
||||
role = metadata.get("source_agent_role") or metadata.get("agent_role") or "worker"
|
||||
role_branch = f"agent/{role}/{self.swarm_id}"
|
||||
self._run_git(["git", "checkout", base_branch], repo_dir)
|
||||
self._run_git(["git", "checkout", "-B", role_branch], repo_dir)
|
||||
files = self._git_repo_files_from_artifact(artifact)
|
||||
if not files:
|
||||
updated_artifacts.append(artifact)
|
||||
continue
|
||||
self._write_project_files_to_repo(repo_dir, files)
|
||||
self._run_git(["git", "add", "-A"], repo_dir)
|
||||
commit_message = f"sub-mode {role} delivery for {self.swarm_id}"
|
||||
commit_result = subprocess.run(
|
||||
["git", "commit", "-m", commit_message],
|
||||
cwd=str(repo_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
if commit_result.returncode != 0 and "nothing to commit" not in (commit_result.stdout + commit_result.stderr).lower():
|
||||
raise RuntimeError(commit_result.stderr.strip() or commit_result.stdout.strip() or "git commit failed")
|
||||
self._run_git(["git", "push", auth_url, role_branch], repo_dir, timeout=180)
|
||||
commit_sha = self._run_git(["git", "rev-parse", "HEAD"], repo_dir).stdout.strip()
|
||||
metadata["git_ref"] = {
|
||||
"provider": metadata.get("git_ref", {}).get("provider") or base_git_ref.get("provider") or "git",
|
||||
"repo_url": origin_url,
|
||||
"base_branch": base_branch,
|
||||
"branch": role_branch,
|
||||
"commit_sha": commit_sha,
|
||||
"git_binding_id": git_context.get("git_binding_id"),
|
||||
}
|
||||
artifact["metadata"] = metadata
|
||||
updated_artifacts.append(artifact)
|
||||
role_branches.append(role_branch)
|
||||
|
||||
if role_branches:
|
||||
delivery_branch = f"delivery/{self.swarm_id}"
|
||||
self._run_git(["git", "checkout", base_branch], repo_dir)
|
||||
self._run_git(["git", "checkout", "-B", delivery_branch], repo_dir)
|
||||
for role_branch in role_branches:
|
||||
merge_result = subprocess.run(
|
||||
["git", "merge", "--no-ff", "--no-edit", role_branch],
|
||||
cwd=str(repo_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=180,
|
||||
)
|
||||
if merge_result.returncode != 0:
|
||||
raise RuntimeError(merge_result.stderr.strip() or merge_result.stdout.strip() or f"git merge failed for {role_branch}")
|
||||
self._run_git(["git", "push", auth_url, delivery_branch], repo_dir, timeout=180)
|
||||
delivery_sha = self._run_git(["git", "rev-parse", "HEAD"], repo_dir).stdout.strip()
|
||||
if self.swarm:
|
||||
context = self._callback_context()
|
||||
context["delivery_branch"] = delivery_branch
|
||||
context["delivery_commit_sha"] = delivery_sha
|
||||
self.swarm.project_context = context
|
||||
self.db.commit()
|
||||
for artifact in updated_artifacts:
|
||||
metadata = artifact.get("metadata") or {}
|
||||
if metadata.get("artifact_layout") == "project_folder":
|
||||
metadata["delivery_ref"] = {
|
||||
"kind": "git_branch",
|
||||
"branch": delivery_branch,
|
||||
"commit_sha": delivery_sha,
|
||||
"project_revision": metadata.get("project_revision", 1),
|
||||
}
|
||||
artifact["metadata"] = metadata
|
||||
|
||||
return updated_artifacts
|
||||
|
||||
def _project_files_from_content(self, role: str, content: str) -> tuple[Dict[str, str], str]:
|
||||
"""Convert a code-oriented response into a minimal project-folder file set."""
|
||||
root_dir = f"{role}-delivery"
|
||||
|
||||
Reference in New Issue
Block a user