Files
agent-skills/pipeline/publisher.py
T
VPS admin 8da8d703da Agent Skills Pipeline v1.0
8-stage pipeline: Scout → Filter → Reader → Extractor → Score → Generator → Reviewer → Publisher
- Scout: GitHub search with token auth + rate limit retry
- Filter: Deterministic rules (language, stars, age, keywords)
- Reader: Incremental context loading (README → docs → examples → code)
- Extractor: LLM workflow extraction with JSON retry
- Score: Rule-based evaluation (no LLM)
- Generator: Standardized Hermes Skill format
- Reviewer: Independent LLM review (separate from generator)
- Publisher: Branch + PR to Gitea

First run: 5 repos discovered, 0 extracted (correct — all frameworks, no workflows)
2026-08-05 05:51:06 +00:00

137 lines
5.3 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 == 200:
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', '')}",
}
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}",
}