"""Stage 8: Publisher — Create branch, commit, open PR on Gitea.""" import json import subprocess import os import tempfile import shutil import datetime def publish_skill(review_result, config): """ Publish approved skill to Gitea repo via git branch + PR. Nothing is merged automatically — human approves. """ if review_result.get("status") != "APPROVED": return { "status": "BLOCKED", "reason": f"Review result: {review_result.get('status', 'unknown')} — {review_result.get('reason', '')}", } gitea_config = config.get("gitea", {}) token = gitea_config.get("token", "") base_url = gitea_config.get("base_url", "http://localhost:3000") owner = gitea_config.get("owner", "tonyjbala") repo_name = gitea_config.get("repo", "agent-skills") clone_url = gitea_config.get("clone_url", f"{base_url}/{owner}/{repo_name}.git") gen = review_result.get("generator_output", {}) skill_name = gen.get("skill_name", "unknown") files = gen.get("files", {}) # Create branch name ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") branch_name = f"skill/{skill_name}-{ts}" with tempfile.TemporaryDirectory() as tmpdir: # Clone repo repo_dir = os.path.join(tmpdir, "agent-skills") result = subprocess.run( ["git", "clone", "--branch", "main", "--single-branch", clone_url, repo_dir], capture_output=True, text=True, timeout=30 ) if result.returncode != 0: # Try without --branch (might not exist yet) result = subprocess.run( ["git", "clone", clone_url, repo_dir], capture_output=True, text=True, timeout=30 ) if result.returncode != 0: return { "status": "CLONE_ERROR", "error": result.stderr[:500], } # Configure git subprocess.run(["git", "config", "user.email", "hermes@agent.local"], cwd=repo_dir) subprocess.run(["git", "config", "user.name", "Hermes Pipeline"], cwd=repo_dir) # Create skill directory skill_dir = os.path.join(repo_dir, "skills", skill_name) os.makedirs(skill_dir, exist_ok=True) # Write files for filename, content in files.items(): filepath = os.path.join(skill_dir, filename) with open(filepath, 'w') as f: f.write(content) # Add and commit subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True) subprocess.run( ["git", "commit", "-m", f"Add Skill: {skill_name}\n\nExtracted from: {gen.get('metadata', {}).get('source_repo', 'unknown')}\nScore: {gen.get('metadata', {}).get('score', 0)}"], cwd=repo_dir, capture_output=True ) # Push branch auth_url = clone_url.replace("http://", f"http://tonyjbala:{token}@") push_result = subprocess.run( ["git", "push", "-u", auth_url, f"main:{branch_name}"], capture_output=True, text=True, timeout=30 ) if push_result.returncode != 0: # Try creating from current branch subprocess.run(["git", "checkout", "-b", branch_name], cwd=repo_dir, capture_output=True) push_result = subprocess.run( ["git", "push", "-u", auth_url, branch_name], capture_output=True, text=True, timeout=30 ) if push_result.returncode != 0: return { "status": "PUSH_ERROR", "error": push_result.stderr[:500], } # Create PR via API pr_url = f"{base_url}/api/v1/repos/{owner}/{repo_name}/pulls" pr_payload = { "title": f"Add Skill: {skill_name}", "body": f"## Skill: {skill_name}\n\n" f"**Goal:** {gen.get('metadata', {}).get('goal', '')}\n" f"**Source:** {gen.get('metadata', {}).get('source_repo', '')}\n" f"**Score:** {gen.get('metadata', {}).get('score', 0)}\n" f"**Confidence:** {gen.get('metadata', {}).get('confidence', 0)}\n" f"**Review:** {review_result.get('reason', '')}\n\n" f"### Files\n" + "".join(f"- `{f}`\n" for f in files.keys()), "head": branch_name, "base": "main", } import requests headers = { "Authorization": f"token {token}", "Content-Type": "application/json", } resp = requests.post(pr_url, json=pr_payload, headers=headers, timeout=15) if resp.status_code in (200, 201): pr_data = resp.json() return { "status": "PUBLISHED", "skill_name": skill_name, "branch": branch_name, "pr_url": pr_data.get("html_url", ""), "pr_number": pr_data.get("index", ""), "message": f"PR opened: {pr_data.get('html_url', '')}", } elif resp.status_code == 409: # PR already exists for this branch return { "status": "PUBLISHED", "skill_name": skill_name, "branch": branch_name, "pr_url": f"{base_url}/{owner}/{repo_name}/pulls", "message": f"PR already exists for branch {branch_name}", } else: return { "status": "PR_ERROR", "http_code": resp.status_code, "error": resp.text[:500], "branch": branch_name, "message": f"Branch pushed but PR creation failed. Pushed branch: {branch_name}", }