feat: sync Ralph stories with Gitea issues
This commit is contained in:
@@ -36,6 +36,22 @@ Meshnet continues to own registration, coverage, existing route selection/load b
|
|||||||
|
|
||||||
The project-owned `ShardEngine` hides llama.cpp internals. A worker loads one exact artifact/recipe/range identity. Default tests use fake/tiny fixtures. Real runs are opt-in, preserve raw metrics, and never download models under `/home`.
|
The project-owned `ShardEngine` hides llama.cpp internals. A worker loads one exact artifact/recipe/range identity. Default tests use fake/tiny fixtures. Real runs are opt-in, preserve raw metrics, and never download models under `/home`.
|
||||||
|
|
||||||
|
## Gitea issue synchronization
|
||||||
|
|
||||||
|
Gitea is a projection of `prd.json`, never a competing source of truth. Before and after every supervised Ralph run, invoke:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/ralph_gitea_sync.py sync
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete Ralph invocation with automatic state reconciliation, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/ralph-gitea-run.sh ralph-tui run --prd .scratch/distributed-gguf-runtime/prd.json --agent claude --model sonnet --iterations 1 --no-tui --no-setup --direct-merge --no-sandbox
|
||||||
|
```
|
||||||
|
|
||||||
|
The sync creates/reconciles one Gitea issue per `DGR-*` story, creates missing labels/milestones, closes issues whose `passes` is true, marks the selected next eligible story `status:in-progress`, and marks blocked stories `status:blocked`. `gitea-issues.json` is a derived mapping only.
|
||||||
|
|
||||||
## Evidence and completion
|
## Evidence and completion
|
||||||
|
|
||||||
Each story writes `/run/media/popov/d/DEV/repos/d-popov.com/AI/.claude/worktrees/distributed-gguf-runtime/.scratch/distributed-gguf-runtime/evidence/<DGR-ID>/README.md` with exact files, commands/results, limitations, identities, and dependent-story handoff. Only `prd.json` may record `passes`; all 55 are false now. Generated Markdown cannot override it. One scoped commit per story is expected during future execution, but this plan materialization is intentionally uncommitted.
|
Each story writes `/run/media/popov/d/DEV/repos/d-popov.com/AI/.claude/worktrees/distributed-gguf-runtime/.scratch/distributed-gguf-runtime/evidence/<DGR-ID>/README.md` with exact files, commands/results, limitations, identities, and dependent-story handoff. Only `prd.json` may record `passes`; DGR-017 and DGR-018 are complete and DGR-019 onward remain false. Generated Markdown and Gitea issues cannot override it. One scoped commit per story is expected during future execution.
|
||||||
|
|||||||
335
.scratch/distributed-gguf-runtime/gitea-issues.json
Normal file
335
.scratch/distributed-gguf-runtime/gitea-issues.json
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
{
|
||||||
|
"repository": "https://git.d-popov.com/popov/neuron-tai",
|
||||||
|
"stories": {
|
||||||
|
"DGR-017": {
|
||||||
|
"number": 1,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/1",
|
||||||
|
"state": "closed",
|
||||||
|
"status": "completed"
|
||||||
|
},
|
||||||
|
"DGR-018": {
|
||||||
|
"number": 2,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/2",
|
||||||
|
"state": "closed",
|
||||||
|
"status": "completed"
|
||||||
|
},
|
||||||
|
"DGR-019": {
|
||||||
|
"number": 3,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/3",
|
||||||
|
"state": "open",
|
||||||
|
"status": "ready"
|
||||||
|
},
|
||||||
|
"DGR-020": {
|
||||||
|
"number": 4,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/4",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-021": {
|
||||||
|
"number": 5,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/5",
|
||||||
|
"state": "open",
|
||||||
|
"status": "in-progress"
|
||||||
|
},
|
||||||
|
"DGR-022": {
|
||||||
|
"number": 6,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/6",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-023": {
|
||||||
|
"number": 7,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/7",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-024": {
|
||||||
|
"number": 8,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/8",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-025": {
|
||||||
|
"number": 9,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/9",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-026": {
|
||||||
|
"number": 10,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/10",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-027": {
|
||||||
|
"number": 11,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/11",
|
||||||
|
"state": "open",
|
||||||
|
"status": "ready"
|
||||||
|
},
|
||||||
|
"DGR-028": {
|
||||||
|
"number": 12,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/12",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-029": {
|
||||||
|
"number": 13,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/13",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-030": {
|
||||||
|
"number": 14,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/14",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-031": {
|
||||||
|
"number": 15,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/15",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-032": {
|
||||||
|
"number": 16,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/16",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-033": {
|
||||||
|
"number": 17,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/17",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-034": {
|
||||||
|
"number": 18,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/18",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-035": {
|
||||||
|
"number": 19,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/19",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-036": {
|
||||||
|
"number": 20,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/20",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-037": {
|
||||||
|
"number": 21,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/21",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-038": {
|
||||||
|
"number": 22,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/22",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-039": {
|
||||||
|
"number": 23,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/23",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-040": {
|
||||||
|
"number": 24,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/24",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-041": {
|
||||||
|
"number": 25,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/25",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-042": {
|
||||||
|
"number": 26,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/26",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-043": {
|
||||||
|
"number": 27,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/27",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-044": {
|
||||||
|
"number": 28,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/28",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-045": {
|
||||||
|
"number": 29,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/29",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-046": {
|
||||||
|
"number": 30,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/30",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-047": {
|
||||||
|
"number": 31,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/31",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-048": {
|
||||||
|
"number": 32,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/32",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-049": {
|
||||||
|
"number": 33,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/33",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-050": {
|
||||||
|
"number": 34,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/34",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-051": {
|
||||||
|
"number": 35,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/35",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-052": {
|
||||||
|
"number": 36,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/36",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-053": {
|
||||||
|
"number": 37,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/37",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-054": {
|
||||||
|
"number": 38,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/38",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-055": {
|
||||||
|
"number": 39,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/39",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-056": {
|
||||||
|
"number": 40,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/40",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-057": {
|
||||||
|
"number": 41,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/41",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-058": {
|
||||||
|
"number": 42,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/42",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-059": {
|
||||||
|
"number": 43,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/43",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-060": {
|
||||||
|
"number": 44,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/44",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-061": {
|
||||||
|
"number": 45,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/45",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-062": {
|
||||||
|
"number": 46,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/46",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-063": {
|
||||||
|
"number": 47,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/47",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-064": {
|
||||||
|
"number": 48,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/48",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-065": {
|
||||||
|
"number": 49,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/49",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-066": {
|
||||||
|
"number": 50,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/50",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-067": {
|
||||||
|
"number": 51,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/51",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-068": {
|
||||||
|
"number": 52,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/52",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-069": {
|
||||||
|
"number": 53,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/53",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-070": {
|
||||||
|
"number": 54,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/54",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
},
|
||||||
|
"DGR-071": {
|
||||||
|
"number": 55,
|
||||||
|
"url": "https://git.d-popov.com/popov/neuron-tai/issues/55",
|
||||||
|
"state": "open",
|
||||||
|
"status": "blocked"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
scripts/ralph-gitea-run.sh
Normal file
12
scripts/ralph-gitea-run.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run one or more supervised Ralph iterations with Gitea state reconciliation.
|
||||||
|
# The canonical PRD remains authoritative; Gitea is updated before and after Ralph.
|
||||||
|
set -u
|
||||||
|
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
python3 scripts/ralph_gitea_sync.py sync
|
||||||
|
ralph_status=0
|
||||||
|
"$@" || ralph_status=$?
|
||||||
|
python3 scripts/ralph_gitea_sync.py sync
|
||||||
|
exit "$ralph_status"
|
||||||
206
scripts/ralph_gitea_sync.py
Normal file
206
scripts/ralph_gitea_sync.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Synchronize the authoritative Ralph PRD with Gitea issues.
|
||||||
|
|
||||||
|
Gitea is a projection of prd.json. Story completion remains authoritative in
|
||||||
|
prd.json; this tool only creates/reconciles issue bodies, labels, milestones,
|
||||||
|
and open/closed/in-progress presentation.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
DEFAULT_FEATURE = ROOT / ".scratch" / "distributed-gguf-runtime"
|
||||||
|
STATUS_LABELS = {
|
||||||
|
"ready": ("status:ready", "Ready for Ralph execution"),
|
||||||
|
"in-progress": ("status:in-progress", "Currently selected by Ralph"),
|
||||||
|
"blocked": ("status:blocked", "Blocked by incomplete dependencies or human gate"),
|
||||||
|
"completed": ("status:completed", "Completed and verified in prd.json"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def credential_for(host: str) -> tuple[str, str]:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "credential", "fill"],
|
||||||
|
input=f"protocol=https\nhost={host}\n\n",
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
values = dict(line.split("=", 1) for line in result.stdout.splitlines() if "=" in line)
|
||||||
|
username, password = values.get("username"), values.get("password")
|
||||||
|
if not password:
|
||||||
|
raise RuntimeError(f"No Git credential available for {host}")
|
||||||
|
return username or "", password
|
||||||
|
|
||||||
|
|
||||||
|
class Gitea:
|
||||||
|
def __init__(self, base_url: str, username: str, password: str, dry_run: bool = False):
|
||||||
|
self.base = base_url.rstrip("/")
|
||||||
|
self.auth = "Basic " + base64.b64encode(f"{username}:{password}".encode()).decode()
|
||||||
|
self.dry_run = dry_run
|
||||||
|
|
||||||
|
def request(self, method: str, path: str, payload: Any = None) -> Any:
|
||||||
|
url = self.base + path
|
||||||
|
body = None if payload is None else json.dumps(payload).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=body,
|
||||||
|
method=method,
|
||||||
|
headers={
|
||||||
|
"Authorization": self.auth,
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if self.dry_run and method not in {"GET", "HEAD"}:
|
||||||
|
print(f"DRY-RUN {method} {path}")
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as response:
|
||||||
|
raw = response.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
detail = exc.read().decode(errors="replace")[:500]
|
||||||
|
raise RuntimeError(f"Gitea {method} {path} -> HTTP {exc.code}: {detail}") from exc
|
||||||
|
|
||||||
|
def paginate(self, path: str) -> list[dict[str, Any]]:
|
||||||
|
result: list[dict[str, Any]] = []
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
separator = "&" if "?" in path else "?"
|
||||||
|
batch = self.request("GET", f"{path}{separator}limit=50&page={page}")
|
||||||
|
if not batch:
|
||||||
|
return result
|
||||||
|
result.extend(batch)
|
||||||
|
if len(batch) < 50:
|
||||||
|
return result
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
|
||||||
|
def color(name: str) -> str:
|
||||||
|
return "#" + hashlib.sha256(name.encode()).hexdigest()[:6]
|
||||||
|
|
||||||
|
|
||||||
|
def load_schema(feature: Path) -> Any:
|
||||||
|
module_path = ROOT / "scripts" / "ralph_prd_schema.py"
|
||||||
|
import importlib.util
|
||||||
|
spec = importlib.util.spec_from_file_location("ralph_prd_schema", module_path)
|
||||||
|
assert spec is not None and spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def issue_body(schema: Any, story: dict[str, Any]) -> str:
|
||||||
|
data = schema.load_prd(DEFAULT_FEATURE / "prd.json")
|
||||||
|
return schema.render_issue_markdown(story, data) + f"\n<!-- ralph-story-id: {story['id']} -->\n"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("command", choices=["sync"])
|
||||||
|
parser.add_argument("--feature-dir", type=Path, default=DEFAULT_FEATURE)
|
||||||
|
parser.add_argument("--repo-url", default="https://git.d-popov.com/popov/neuron-tai")
|
||||||
|
parser.add_argument("--dry-run", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
feature = args.feature_dir.resolve()
|
||||||
|
prd_path = feature / "prd.json"
|
||||||
|
data = json.loads(prd_path.read_text())
|
||||||
|
stories = data["userStories"]
|
||||||
|
repo = urllib.parse.urlparse(args.repo_url)
|
||||||
|
if not repo.hostname or not repo.path.strip("/"):
|
||||||
|
raise SystemExit("--repo-url must be an HTTPS Gitea repository URL")
|
||||||
|
owner, name = repo.path.strip("/").removesuffix(".git").split("/", 1)
|
||||||
|
api_base = f"{repo.scheme}://{repo.hostname}/api/v1/repos/{owner}/{name}"
|
||||||
|
username, password = credential_for(repo.hostname)
|
||||||
|
gitea = Gitea(api_base, username, password, args.dry_run)
|
||||||
|
schema = load_schema(feature)
|
||||||
|
schema_errors = schema.validate_backlog(data)
|
||||||
|
if schema_errors:
|
||||||
|
raise SystemExit("PRD validation failed:\n" + "\n".join(schema_errors))
|
||||||
|
|
||||||
|
labels = {x["name"]: x for x in gitea.paginate("/labels")}
|
||||||
|
milestones = {x["title"]: x for x in gitea.paginate("/milestones?state=all")}
|
||||||
|
issue_list = gitea.paginate("/issues?state=all")
|
||||||
|
by_story = {}
|
||||||
|
for issue in issue_list:
|
||||||
|
marker = f"<!-- ralph-story-id: DGR-"
|
||||||
|
body = issue.get("body") or ""
|
||||||
|
if marker in body:
|
||||||
|
story_id = body.split(marker, 1)[1].split(" -->", 1)[0]
|
||||||
|
by_story["DGR-" + story_id] = issue
|
||||||
|
elif issue.get("title", "").startswith("DGR-"):
|
||||||
|
by_story[issue["title"].split(":", 1)[0]] = issue
|
||||||
|
|
||||||
|
def ensure_label(name_: str, description: str = "") -> dict[str, Any]:
|
||||||
|
if name_ in labels:
|
||||||
|
return labels[name_]
|
||||||
|
created = gitea.request("POST", "/labels", {"name": name_, "color": color(name_), "description": description})
|
||||||
|
if args.dry_run:
|
||||||
|
created = {"id": -len(labels) - 1, "name": name_}
|
||||||
|
labels[name_] = created
|
||||||
|
return created
|
||||||
|
|
||||||
|
for story in stories:
|
||||||
|
for label in story["labels"]:
|
||||||
|
ensure_label(label)
|
||||||
|
for label, description in STATUS_LABELS.values():
|
||||||
|
ensure_label(label, description)
|
||||||
|
for milestone in data.get("milestones", []):
|
||||||
|
title = milestone["id"]
|
||||||
|
if title not in milestones:
|
||||||
|
milestones[title] = gitea.request("POST", "/milestones", {"title": title, "description": milestone["name"] + ": " + milestone["outcome"]})
|
||||||
|
if args.dry_run:
|
||||||
|
milestones[title] = {"id": -len(milestones), "title": title}
|
||||||
|
|
||||||
|
passed = {s["id"] for s in stories if s.get("passes")}
|
||||||
|
next_story = next(
|
||||||
|
(
|
||||||
|
s
|
||||||
|
for s in stories
|
||||||
|
if not s.get("passes")
|
||||||
|
and s.get("triage") == "ready-for-agent"
|
||||||
|
and all(d in passed for d in s["dependsOn"])
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
mapping = {}
|
||||||
|
for story in stories:
|
||||||
|
sid = story["id"]
|
||||||
|
desired_status = "completed" if story.get("passes") else ("in-progress" if next_story and sid == next_story["id"] else ("ready" if all(d in passed for d in story["dependsOn"]) else "blocked"))
|
||||||
|
label_names = list(dict.fromkeys(story["labels"] + [STATUS_LABELS[desired_status][0]]))
|
||||||
|
label_ids = [ensure_label(label)["id"] for label in label_names]
|
||||||
|
milestone_id = milestones[story["milestone"]]["id"]
|
||||||
|
title = f"{sid}: {story['title']}"
|
||||||
|
body = issue_body(schema, story)
|
||||||
|
issue = by_story.get(sid)
|
||||||
|
payload = {"title": title, "body": body, "labels": label_ids, "milestone": milestone_id, "state": "closed" if story.get("passes") else "open"}
|
||||||
|
if issue is None:
|
||||||
|
issue = gitea.request("POST", "/issues", payload)
|
||||||
|
print(f"created {sid} #{issue.get('number')} {desired_status}")
|
||||||
|
else:
|
||||||
|
issue = gitea.request("PATCH", f"/issues/{issue['number']}", payload)
|
||||||
|
print(f"reconciled {sid} #{issue.get('number')} {desired_status}")
|
||||||
|
mapping[sid] = {"number": issue.get("number"), "url": issue.get("html_url"), "state": "closed" if story.get("passes") else "open", "status": desired_status}
|
||||||
|
|
||||||
|
if not args.dry_run:
|
||||||
|
(feature / "gitea-issues.json").write_text(json.dumps({"repository": args.repo_url, "stories": mapping}, indent=2) + "\n")
|
||||||
|
print(f"synced={len(stories)} next={next_story['id'] if next_story else 'none'} dry_run={args.dry_run}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user