"""Stage 8: Publisher — Create branch, commit, open PR on Gitea.""" import subprocess import os import tempfile import datetime import requests 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: repo_dir = os.path.join(tmpdir, "agent-skills") # Clone repo result = subprocess.run( ["git", "clone", "--branch", "main", "--depth", "1", clone_url, repo_dir], capture_output=True, text=True, timeout=30 ) if result.returncode != 0: result = subprocess.run( ["git", "clone", "--depth", "1", 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) # Check for duplicates in skills/ directory skills_dir = os.path.join(repo_dir, "skills") existing_skills = [] if os.path.isdir(skills_dir): existing_skills = [d for d in os.listdir(skills_dir) if os.path.isdir(os.path.join(skills_dir, d))] if skill_name in existing_skills: return { "status": "SKIP", "reason": f"Skill '{skill_name}' already exists in skills/ directory", } # Create skill directory skill_dir = os.path.join(repo_dir, "skills", skill_name) os.makedirs(skill_dir, exist_ok=True) # Write skill files for filename, content in files.items(): filepath = os.path.join(skill_dir, filename) with open(filepath, "w") as f: f.write(content) # Verify files were written written_files = [] for root, dirs, fnames in os.walk(skill_dir): for fn in fnames: written_files.append(os.path.join(root, fn)) if not written_files: return {"status": "EMPTY_SKILL", "reason": "No files written to skill directory"} # Stage and commit add_result = subprocess.run( ["git", "add", "skills/"], cwd=repo_dir, capture_output=True, text=True ) # Check if there are actually staged changes status_result = subprocess.run( ["git", "diff", "--cached", "--name-only"], cwd=repo_dir, capture_output=True, text=True ) staged_files = status_result.stdout.strip().split("\n") if status_result.stdout.strip() else [] if not staged_files: # Nothing to commit — files might already exist. Force add. subprocess.run(["git", "add", "-f", "skills/"], cwd=repo_dir, capture_output=True, text=True) status_result = subprocess.run( ["git", "diff", "--cached", "--name-only"], cwd=repo_dir, capture_output=True, text=True ) staged_files = status_result.stdout.strip().split("\n") if status_result.stdout.strip() else [] if not staged_files: return { "status": "NO_CHANGES", "reason": f"No new files to commit for {skill_name}. Files already exist in repo.", } commit_result = 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, text=True ) if commit_result.returncode != 0: return { "status": "COMMIT_ERROR", "error": commit_result.stderr[:500], } # Checkout new branch checkout_result = subprocess.run( ["git", "checkout", "-b", branch_name], cwd=repo_dir, capture_output=True, text=True ) if checkout_result.returncode != 0: return { "status": "CHECKOUT_ERROR", "error": checkout_result.stderr[:500], } # Push branch auth_url = clone_url.replace("http://", f"http://tonyjbala:{token}@") push_result = subprocess.run( ["git", "push", "-u", auth_url, branch_name], cwd=repo_dir, 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\n" f"### Files\n" + "".join(f"- `{f}`\n" for f in files.keys()) ), "head": branch_name, "base": "main", } 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: 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}", }