welcome/.gitea/sync_beads.py
rootiest ab979e5e19
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:47:09 -05:00

134 lines
4.7 KiB
Python
Raw 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_num = existing[bid]["number"]
print(
f"🔒 Archiving Gitea Issue #{issue_num} (Tombstone found for {bid})"
)
# We PATCH the issue to close it and rename it
# This avoids the 403 error while removing it from your 'Open' list
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,
)
if patch_resp.status_code == 200:
print(f"✅ Successfully archived {bid}")
deleted_count += 1
else:
print(
f"⚠️ Could not archive {bid}: {patch_resp.status_code}"
)
else:
print(f"👻 Skipping tombstone: {bid} (Already gone/archived)")
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()