welcome/.gitea/sync_beads.py
rootiest 23beb115fa
All checks were successful
Sync Beads to Gitea Issues / sync-beads (push) Successful in 7s
feat: integrate beads
bd sync: 2026-01-19 21:41:13
2026-01-19 21:51:41 -05:00

155 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import os
import requests
import sys
# Configuration from Environment
TOKEN = os.getenv("GITEA_TOKEN")
URL = os.getenv("GITEA_URL")
REPO = os.getenv("REPO_NAME")
HEADERS = {"Authorization": f"token {TOKEN}", "Content-Type": "application/json"}
def sync():
beads_path = ".beads/issues.jsonl"
if not os.path.exists(beads_path):
print(f"❌ ERROR: {beads_path} not found.")
sys.exit(1)
print(f"🔍 Reading Beads from: {beads_path}")
# 1. Fetch existing issues from Gitea
try:
api_url = f"{URL}/api/v1/repos/{REPO}/issues?state=all"
resp = requests.get(api_url, headers=HEADERS)
resp.raise_for_status()
# Map by [ID] to identify existing ones
existing = {
i["title"].split("]")[0][1:]: i for i in resp.json() if "]" in i["title"]
}
print(f"📡 Found {len(existing)} existing issues in Gitea.")
except Exception as e:
print(f"❌ Gitea API Connection Failed: {e}")
sys.exit(1)
# 2. Parse the Beads JSONL file
processed_count = 0
skipped_count = 0
deleted_count = 0
with open(beads_path, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
bid = data.get("id")
status = data.get("status")
title = data.get("title")
itype = data.get("issue_type", "unknown")
if not bid:
print("⚠️ Skipping line: No ID found.")
continue
# --- DELETION / TOMBSTONE LOGIC ---
if status == "tombstone":
if bid in existing:
issue = existing[bid]
issue_num = issue["number"]
# Check if it's already closed/archived in Gitea
if issue["state"] == "closed" and issue["title"].startswith(
"[DELETED]"
):
print(
f"👻 Skipping tombstone: {bid} (Already archived in Gitea)"
)
skipped_count += 1
continue
print(
f"🔒 Archiving Gitea Issue #{issue_num} (Tombstone found for {bid})"
)
archive_payload = {
"state": "closed",
"title": f"[DELETED] {title}",
}
patch_resp = requests.patch(
f"{URL}/api/v1/repos/{REPO}/issues/{issue_num}",
headers=HEADERS,
json=archive_payload,
)
# Accept 200 or 201 as success
if patch_resp.status_code in [200, 201]:
print(f"✅ Successfully archived {bid}")
# Add a comment about the deletion
comment_payload = {
"body": f"⚠️ This issue was deleted in **Beads** (ID: `{bid}`). It has been automatically closed and renamed for archival purposes."
}
comment_url = (
f"{URL}/api/v1/repos/{REPO}/issues/{issue_num}/comments"
)
requests.post(
comment_url, headers=HEADERS, json=comment_payload
)
deleted_count += 1
else:
print(
f"⚠️ Could not archive {bid}: {patch_resp.status_code}"
)
else:
print(f"👻 Skipping tombstone: {bid} (Not found in Gitea)")
skipped_count += 1
continue
# --- STANDARD SYNC LOGIC ---
processed_count += 1
gitea_state = (
"closed" if status in ["closed", "done", "finished"] else "open"
)
payload = {
"title": f"[{bid}] {title}",
"body": f"{data.get('description', 'No description provided.')}\n\n---\n**Beads ID:** `{bid}`\n**Type:** `{itype}`",
"state": gitea_state,
}
if bid in existing:
print(f"✅ Updating {bid} (Issue #{existing[bid]['number']})")
requests.patch(
f"{URL}/api/v1/repos/{REPO}/issues/{existing[bid]['number']}",
headers=HEADERS,
json=payload,
)
else:
print(f" Creating New Gitea Issue for {bid}")
r = requests.post(
f"{URL}/api/v1/repos/{REPO}/issues",
headers=HEADERS,
json=payload,
)
r.raise_for_status()
except Exception as e:
print(f"⚠️ Error processing bead line: {e}")
print(
f"🏁 Finished. Active: {processed_count}, Deleted: {deleted_count}, Skipped: {skipped_count}"
)
# Safety check to ensure we didn't process a completely empty file
if (processed_count + deleted_count + skipped_count) == 0:
print("❌ ERROR: File was found but it was EMPTY.")
sys.exit(1)
if __name__ == "__main__":
sync()