feat: feedback_api — Forgejo label management + issue filing + attachment upload
This commit is contained in:
parent
cb1131f23c
commit
bdedeb5305
2 changed files with 131 additions and 0 deletions
|
|
@ -4,12 +4,14 @@ Called directly from app/feedback.py now; wrappable in a FastAPI route later.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
_ROOT = Path(__file__).parent.parent
|
_ROOT = Path(__file__).parent.parent
|
||||||
|
|
@ -125,3 +127,68 @@ def build_issue_body(form: dict, context: dict, attachments: dict) -> str:
|
||||||
lines += ["---", f"*Submitted by: {attachments['submitter']}*"]
|
lines += ["---", f"*Submitted by: {attachments['submitter']}*"]
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_labels(
|
||||||
|
label_names: list[str], base_url: str, headers: dict, repo: str
|
||||||
|
) -> list[int]:
|
||||||
|
"""Look up or create Forgejo labels by name. Returns list of IDs."""
|
||||||
|
_COLORS = {
|
||||||
|
"beta-feedback": "#0075ca",
|
||||||
|
"needs-triage": "#e4e669",
|
||||||
|
"bug": "#d73a4a",
|
||||||
|
"feature-request": "#a2eeef",
|
||||||
|
"question": "#d876e3",
|
||||||
|
}
|
||||||
|
resp = requests.get(f"{base_url}/repos/{repo}/labels", headers=headers, timeout=10)
|
||||||
|
existing = {lb["name"]: lb["id"] for lb in resp.json()} if resp.ok else {}
|
||||||
|
ids: list[int] = []
|
||||||
|
for name in label_names:
|
||||||
|
if name in existing:
|
||||||
|
ids.append(existing[name])
|
||||||
|
else:
|
||||||
|
r = requests.post(
|
||||||
|
f"{base_url}/repos/{repo}/labels",
|
||||||
|
headers=headers,
|
||||||
|
json={"name": name, "color": _COLORS.get(name, "#ededed")},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if r.ok:
|
||||||
|
ids.append(r.json()["id"])
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
def create_forgejo_issue(title: str, body: str, labels: list[str]) -> dict:
|
||||||
|
"""Create a Forgejo issue. Returns {"number": int, "url": str}."""
|
||||||
|
token = os.environ.get("FORGEJO_API_TOKEN", "")
|
||||||
|
repo = os.environ.get("FORGEJO_REPO", "pyr0ball/peregrine")
|
||||||
|
base = os.environ.get("FORGEJO_API_URL", "https://git.opensourcesolarpunk.com/api/v1")
|
||||||
|
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
||||||
|
label_ids = _ensure_labels(labels, base, headers, repo)
|
||||||
|
resp = requests.post(
|
||||||
|
f"{base}/repos/{repo}/issues",
|
||||||
|
headers=headers,
|
||||||
|
json={"title": title, "body": body, "labels": label_ids},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
return {"number": data["number"], "url": data["html_url"]}
|
||||||
|
|
||||||
|
|
||||||
|
def upload_attachment(
|
||||||
|
issue_number: int, image_bytes: bytes, filename: str = "screenshot.png"
|
||||||
|
) -> str:
|
||||||
|
"""Upload a screenshot to an existing Forgejo issue. Returns attachment URL."""
|
||||||
|
token = os.environ.get("FORGEJO_API_TOKEN", "")
|
||||||
|
repo = os.environ.get("FORGEJO_REPO", "pyr0ball/peregrine")
|
||||||
|
base = os.environ.get("FORGEJO_API_URL", "https://git.opensourcesolarpunk.com/api/v1")
|
||||||
|
headers = {"Authorization": f"token {token}"}
|
||||||
|
resp = requests.post(
|
||||||
|
f"{base}/repos/{repo}/issues/{issue_number}/assets",
|
||||||
|
headers=headers,
|
||||||
|
files={"attachment": (filename, image_bytes, "image/png")},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json().get("browser_download_url", "")
|
||||||
|
|
|
||||||
|
|
@ -178,3 +178,67 @@ def test_build_issue_body_listings_shown():
|
||||||
body = build_issue_body(form, {}, {"listings": listings})
|
body = build_issue_body(form, {}, {"listings": listings})
|
||||||
assert "CSM" in body
|
assert "CSM" in body
|
||||||
assert "Acme" in body
|
assert "Acme" in body
|
||||||
|
|
||||||
|
|
||||||
|
# ── Forgejo API ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@patch("scripts.feedback_api.requests.get")
|
||||||
|
@patch("scripts.feedback_api.requests.post")
|
||||||
|
def test_ensure_labels_uses_existing(mock_post, mock_get):
|
||||||
|
from scripts.feedback_api import _ensure_labels
|
||||||
|
mock_get.return_value.ok = True
|
||||||
|
mock_get.return_value.json.return_value = [
|
||||||
|
{"name": "beta-feedback", "id": 1},
|
||||||
|
{"name": "bug", "id": 2},
|
||||||
|
]
|
||||||
|
ids = _ensure_labels(
|
||||||
|
["beta-feedback", "bug"],
|
||||||
|
"https://example.com/api/v1", {"Authorization": "token x"}, "owner/repo"
|
||||||
|
)
|
||||||
|
assert ids == [1, 2]
|
||||||
|
mock_post.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("scripts.feedback_api.requests.get")
|
||||||
|
@patch("scripts.feedback_api.requests.post")
|
||||||
|
def test_ensure_labels_creates_missing(mock_post, mock_get):
|
||||||
|
from scripts.feedback_api import _ensure_labels
|
||||||
|
mock_get.return_value.ok = True
|
||||||
|
mock_get.return_value.json.return_value = []
|
||||||
|
mock_post.return_value.ok = True
|
||||||
|
mock_post.return_value.json.return_value = {"id": 99}
|
||||||
|
ids = _ensure_labels(
|
||||||
|
["needs-triage"],
|
||||||
|
"https://example.com/api/v1", {"Authorization": "token x"}, "owner/repo"
|
||||||
|
)
|
||||||
|
assert 99 in ids
|
||||||
|
|
||||||
|
|
||||||
|
@patch("scripts.feedback_api._ensure_labels", return_value=[1, 2])
|
||||||
|
@patch("scripts.feedback_api.requests.post")
|
||||||
|
def test_create_forgejo_issue_success(mock_post, mock_labels, monkeypatch):
|
||||||
|
from scripts.feedback_api import create_forgejo_issue
|
||||||
|
monkeypatch.setenv("FORGEJO_API_TOKEN", "testtoken")
|
||||||
|
monkeypatch.setenv("FORGEJO_REPO", "owner/repo")
|
||||||
|
monkeypatch.setenv("FORGEJO_API_URL", "https://example.com/api/v1")
|
||||||
|
mock_post.return_value.status_code = 201
|
||||||
|
mock_post.return_value.raise_for_status = lambda: None
|
||||||
|
mock_post.return_value.json.return_value = {"number": 42, "html_url": "https://example.com/issues/42"}
|
||||||
|
result = create_forgejo_issue("Test issue", "body text", ["beta-feedback", "bug"])
|
||||||
|
assert result["number"] == 42
|
||||||
|
assert "42" in result["url"]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("scripts.feedback_api.requests.post")
|
||||||
|
def test_upload_attachment_returns_url(mock_post, monkeypatch):
|
||||||
|
from scripts.feedback_api import upload_attachment
|
||||||
|
monkeypatch.setenv("FORGEJO_API_TOKEN", "testtoken")
|
||||||
|
monkeypatch.setenv("FORGEJO_REPO", "owner/repo")
|
||||||
|
monkeypatch.setenv("FORGEJO_API_URL", "https://example.com/api/v1")
|
||||||
|
mock_post.return_value.status_code = 201
|
||||||
|
mock_post.return_value.raise_for_status = lambda: None
|
||||||
|
mock_post.return_value.json.return_value = {
|
||||||
|
"uuid": "abc", "browser_download_url": "https://example.com/assets/abc"
|
||||||
|
}
|
||||||
|
url = upload_attachment(42, b"\x89PNG", "screenshot.png")
|
||||||
|
assert url == "https://example.com/assets/abc"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue