Files
agent-skills/pipeline/publisher.py
T
Epictetus dc40d4c0db Pipeline v2: deterministic reviewer, implementation extraction, publisher fix
- Reader: discover workflow files in nested dirs (agents/, workflows/, examples/)
- Reader: load source code, config, deps — not just docs
- Extractor: prompt demands concrete implementation details (files, deps, code)
- Scorer: removed general_purpose check (5/6 checks, score 1.0)
- Generator: includes Setup, Key Files, Implementation Details sections
- Reviewer: replaced LLM review with 8 deterministic structural checks
- Publisher: handle 409 duplicate PR gracefully as success
- 5 skills published as PRs #6-#10 on Gitea
2026-08-05 13:58:38 +00:00

146 lines
5.7 KiB
Python

"""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}",
}