7f496feb90
- Skip skills already in skills/ directory (no duplicate PRs) - Run.py shows SKIP status with reason - Fixed: was re-publishing same 5 skills every run
200 lines
7.5 KiB
Python
200 lines
7.5 KiB
Python
"""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}",
|
|
}
|