Files
agent-skills/pipeline/publisher.py
Epictetus 7f496feb90 Publisher dedup check + run.py skip display
- 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
2026-08-05 15:49:57 +00:00

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