#!/usr/bin/env python3 """ Sync BloomBase Notion feature tracker → Forgejo issues. Notion is source of truth. Runs one-way: Notion → Forgejo. Idempotent: tracks Notion page IDs in issue body to avoid duplicates. Usage: FORGEJO_TOKEN= python3 scripts/sync_notion_to_forgejo.py (Notion access via Claude MCP — run from a Claude Code session instead) """ import json import os import re import urllib.request import urllib.parse FORGEJO_BASE = "https://git.kerkman.io/api/v1" FORGEJO_REPO = "stephan/bloombase" NOTION_MARKER = "notion-id:" LABEL_MAP = { "Aesthetics": 1, "Functionality": 2, "Bug": 3, } STATUS_MAP = { "Open": "open", "In progress": "open", # + "In progress" label (id=4) "Done": "open", # stays open until Naomi ticks Verified } IN_PROGRESS_LABEL_ID = 4 def forgejo(method, path, data=None): token = os.environ["FORGEJO_TOKEN"] url = f"{FORGEJO_BASE}{path}" body = json.dumps(data).encode() if data else None req = urllib.request.Request( url, data=body, method=method, headers={ "Authorization": f"token {token}", "Content-Type": "application/json", } ) with urllib.request.urlopen(req) as r: return json.loads(r.read()) def get_existing_issues(): """Return dict of notion_page_id → issue number for already-synced issues.""" issues = forgejo("GET", f"/repos/{FORGEJO_REPO}/issues?type=issues&state=open&limit=50") issues += forgejo("GET", f"/repos/{FORGEJO_REPO}/issues?type=issues&state=closed&limit=50") mapping = {} for issue in issues: body = issue.get("body") or "" m = re.search(rf"{NOTION_MARKER}(\S+)", body) if m: mapping[m.group(1)] = issue["number"] return mapping def sync_item(item, existing): """Create or update a single Forgejo issue from a Notion item dict.""" notion_id = item["id"] title = item["title"].strip() category = item.get("category") status = item.get("status", "Open") verified = item.get("verified", False) notion_url = item["url"] has_screenshot = item.get("has_screenshot", False) labels = [] if category and category in LABEL_MAP: labels.append(LABEL_MAP[category]) if status == "In progress": labels.append(IN_PROGRESS_LABEL_ID) screenshot_note = "\n\nšŸ“Ž Screenshot attached in Notion" if has_screenshot else "" if status == "Done" and not verified: verification_note = "\n\nā³ *Awaiting Naomi's verification*" elif status == "Done" and verified: verification_note = "\n\nāœ… *Verified by Naomi*" else: verification_note = "" body = ( f"*Synced from [🌸 BloomBase Notion]({notion_url})*" f"{screenshot_note}" f"{verification_note}" f"\n\n" ) # Only close the Forgejo issue once Naomi has verified if status == "Done" and verified: state = "closed" else: state = STATUS_MAP.get(status, "open") if notion_id in existing: issue_num = existing[notion_id] forgejo("PATCH", f"/repos/{FORGEJO_REPO}/issues/{issue_num}", { "title": title, "body": body, "state": state, "labels": labels, }) print(f" updated #{issue_num}: {title[:60]}") else: issue = forgejo("POST", f"/repos/{FORGEJO_REPO}/issues", { "title": title, "body": body, "labels": labels, }) if state == "closed": forgejo("PATCH", f"/repos/{FORGEJO_REPO}/issues/{issue['number']}", {"state": "closed"}) print(f" created #{issue['number']}: {title[:60]}") if __name__ == "__main__": # Items passed in as JSON via stdin or hard-coded for manual runs. # In practice: run from a Claude Code session using the Notion MCP, # build the items list from MCP data, then call sync_item() for each. print("Run this from a Claude Code session — Notion data comes via MCP.") print("See: scripts/sync_notion_to_forgejo.py sync_item() for the API contract.")