campaign: forgejo issues sync script; issues are the published board
This commit is contained in:
parent
3bdc83fe29
commit
35269d0083
2 changed files with 106 additions and 0 deletions
|
|
@ -34,6 +34,9 @@ A 4-agent crew (defined in `~/.claude/agents/re-*.md`) runs the exploration:
|
|||
**re-scribe** (files the note, links it, commits).
|
||||
|
||||
- `campaign/board.md` — live status board (start here).
|
||||
- **Live tracking = Forgejo issues** on `alex/sots-re` (labels `status/*` are the kanban
|
||||
columns; `type/*`, `conf/*`). `campaign/board.md` is the editable mirror — publish with
|
||||
`scripts/forgejo_campaign.py bootstrap` (needs `FORGEJO_TOKEN`).
|
||||
- `campaign/backlog.md` — prioritized target queue.
|
||||
- `campaign/open-questions.md` — unresolved threads.
|
||||
- `findings/_template.md` — the record format every finding follows.
|
||||
|
|
|
|||
103
scripts/forgejo_campaign.py
Executable file
103
scripts/forgejo_campaign.py
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Sync the SOTS1 RE campaign board (campaign/board.md) to Forgejo issues + labels.
|
||||
Forgejo 10.x has no Projects API, so issues are the source of truth and status labels
|
||||
are the kanban columns. Idempotent: matches issues by exact title.
|
||||
|
||||
FORGEJO_TOKEN=... scripts/forgejo_campaign.py bootstrap # ensure labels, create/sync issues
|
||||
FORGEJO_TOKEN=... scripts/forgejo_campaign.py status # print issues grouped by status
|
||||
"""
|
||||
import json, os, re, sys, urllib.request, urllib.parse
|
||||
|
||||
API = os.environ.get("FORGEJO_API", "http://chonkers:2112/api/v1")
|
||||
REPO = os.environ.get("FORGEJO_REPO", "alex/sots-re")
|
||||
TOKEN = os.environ.get("FORGEJO_TOKEN") or sys.exit("set FORGEJO_TOKEN")
|
||||
BOARD = os.path.join(os.path.dirname(__file__), "..", "campaign", "board.md")
|
||||
|
||||
LABELS = { # name: (color, description)
|
||||
"status/backlog": ("e4e669", "queued target"),
|
||||
"status/in-progress": ("fbca04", "being mapped"),
|
||||
"status/mapped": ("0e8a16", "analyst done; awaiting verification"),
|
||||
"status/verified": ("1d76db", "re-verifier stamped with evidence"),
|
||||
"status/blocked": ("b60205", "blocked"),
|
||||
"type/object": ("5319e7", "object model / class"),
|
||||
"type/control-flow": ("d93f0b", "entry / loops / dispatch"),
|
||||
"type/subsystem": ("0052cc", "subsystem"),
|
||||
"type/meta": ("bfdadc", "campaign meta / inventories"),
|
||||
"type/verify": ("c2e0c6", "verification task"),
|
||||
"conf/low": ("f9d0c4", "low confidence"),
|
||||
"conf/med": ("fef2c0", "medium confidence"),
|
||||
"conf/high": ("c5def5", "high confidence"),
|
||||
}
|
||||
|
||||
def req(method, path, data=None, params=None):
|
||||
url = f"{API}{path}" + (("?" + urllib.parse.urlencode(params)) if params else "")
|
||||
body = json.dumps(data).encode() if data is not None else None
|
||||
r = urllib.request.Request(url, data=body, method=method,
|
||||
headers={"Authorization": f"token {TOKEN}", "Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(r) as resp:
|
||||
t = resp.read()
|
||||
return json.loads(t) if t else None
|
||||
|
||||
def all_pages(path, **params):
|
||||
out, page = [], 1
|
||||
while True:
|
||||
chunk = req("GET", path, params=dict(params, limit=100, page=page)) or []
|
||||
out += chunk
|
||||
if len(chunk) < 100: return out
|
||||
page += 1
|
||||
|
||||
def ensure_labels():
|
||||
have = {l["name"]: l for l in all_pages(f"/repos/{REPO}/labels")}
|
||||
for name, (color, desc) in LABELS.items():
|
||||
if name not in have:
|
||||
have[name] = req("POST", f"/repos/{REPO}/labels", {"name": name, "color": color, "description": desc})
|
||||
print(" + label", name)
|
||||
return {n: l["id"] for n, l in have.items()}
|
||||
|
||||
def parse_board():
|
||||
rows = []
|
||||
for line in open(BOARD, encoding="utf-8"):
|
||||
if not line.startswith("|") or line.startswith("| Target") or line.startswith("|---"): continue
|
||||
c = [x.strip() for x in line.strip().strip("|").split("|")]
|
||||
if len(c) < 7: continue
|
||||
target, typ, status, conf, cov, upd, notes = c[:7]
|
||||
rows.append(dict(title=target.replace("`", "").strip(), type=typ, status=status,
|
||||
conf=conf, cov=cov, upd=upd, notes=notes))
|
||||
return rows
|
||||
|
||||
def want_labels(row, ids):
|
||||
names = [f"status/{row['status']}", f"type/{row['type']}"]
|
||||
if row["conf"] in ("low", "med", "high"): names.append(f"conf/{row['conf']}")
|
||||
return [ids[n] for n in names if n in ids]
|
||||
|
||||
def body_for(row):
|
||||
b = [f"**Type:** {row['type']} · **Status:** {row['status']} · **Confidence:** {row['conf']} · **Coverage:** {row['cov']} · **Updated:** {row['upd']}", "",
|
||||
row["notes"] or "", "", "_Tracked from `campaign/board.md`; findings under `findings/`._"]
|
||||
return "\n".join(b)
|
||||
|
||||
def bootstrap():
|
||||
ids = ensure_labels()
|
||||
existing = {i["title"]: i for i in all_pages(f"/repos/{REPO}/issues", state="all", type="issues")}
|
||||
for row in parse_board():
|
||||
labels = want_labels(row, ids)
|
||||
if row["title"] in existing:
|
||||
n = existing[row["title"]]["number"]
|
||||
req("PUT", f"/repos/{REPO}/issues/{n}/labels", {"labels": labels})
|
||||
req("PATCH", f"/repos/{REPO}/issues/{n}", {"body": body_for(row),
|
||||
"state": "closed" if row["status"] == "verified" else "open"})
|
||||
print(f" ~ #{n} {row['title']} -> {row['status']}")
|
||||
else:
|
||||
i = req("POST", f"/repos/{REPO}/issues", {"title": row["title"], "body": body_for(row), "labels": labels})
|
||||
print(f" + #{i['number']} {row['title']} [{row['status']}]")
|
||||
|
||||
def status():
|
||||
groups = {}
|
||||
for i in all_pages(f"/repos/{REPO}/issues", state="all", type="issues"):
|
||||
st = next((l["name"].split("/",1)[1] for l in i["labels"] if l["name"].startswith("status/")), "?")
|
||||
groups.setdefault(st, []).append(f"#{i['number']} {i['title']}")
|
||||
for st in ("backlog","in-progress","mapped","verified","blocked","?"):
|
||||
if st in groups:
|
||||
print(f"[{st}] ({len(groups[st])})"); [print(" ", x) for x in groups[st]]
|
||||
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
|
||||
{"bootstrap": bootstrap, "status": status}[cmd]()
|
||||
Loading…
Add table
Reference in a new issue