feat(robomp): accept github issue urls in manual triage

parse_issue_ref now accepts owner/repo#NN or a github issue url (with or without scheme, www., trailing slash, query, fragment). The dashboard trigger, CLI, and replay endpoint pick this up automatically; UI hints updated to match.
This commit is contained in:
can1357
2026-05-25 19:33:39 +02:00
parent d351512b2b
commit aca68ad3a9
3 changed files with 39 additions and 7 deletions
+11 -3
View File
@@ -14,6 +14,11 @@ from robomp.db import INACTIVE_EVENT_STATES, Database, EventRow, issue_key
from robomp.github_backend import GitHubBackend
_ISSUE_REF = re.compile(r"^(?P<owner>[^/\s]+)/(?P<repo>[^#\s]+)#(?P<number>\d+)$")
_ISSUE_URL = re.compile(
r"^(?:https?://)?(?:www\.)?github\.com/"
r"(?P<owner>[^/\s]+)/(?P<repo>[^/\s]+)/issues/(?P<number>\d+)"
r"(?:[/?#].*)?$"
)
class InvalidIssueRef(ValueError):
@@ -44,10 +49,13 @@ class ManualTriageTimeout(TimeoutError):
def parse_issue_ref(ref: str) -> tuple[str, int]:
"""Parse `owner/repo#NN` into `("owner/repo", NN)`."""
match = _ISSUE_REF.match(ref.strip())
"""Parse `owner/repo#NN` or a github issue url into `("owner/repo", NN)`."""
cleaned = ref.strip()
match = _ISSUE_REF.match(cleaned) or _ISSUE_URL.match(cleaned)
if match is None:
raise InvalidIssueRef(f"expected owner/repo#NN, got {ref!r}")
raise InvalidIssueRef(
f"expected owner/repo#NN or https://github.com/owner/repo/issues/NN, got {ref!r}"
)
return f"{match.group('owner')}/{match.group('repo')}", int(match.group("number"))
+25 -1
View File
@@ -271,8 +271,32 @@ def test_parse_issue_ref_accepts_owner_repo_hash_number() -> None:
assert parse_issue_ref(" octo/widget#42 ") == ("octo/widget", 42)
def test_parse_issue_ref_accepts_github_issue_urls() -> None:
cases = (
"https://github.com/can1357/oh-my-pi/issues/1348",
"http://github.com/can1357/oh-my-pi/issues/1348",
"github.com/can1357/oh-my-pi/issues/1348",
"https://www.github.com/can1357/oh-my-pi/issues/1348",
"https://github.com/can1357/oh-my-pi/issues/1348/",
"https://github.com/can1357/oh-my-pi/issues/1348?foo=bar",
"https://github.com/can1357/oh-my-pi/issues/1348#issuecomment-99",
" https://github.com/can1357/oh-my-pi/issues/1348 ",
)
for case in cases:
assert parse_issue_ref(case) == ("can1357/oh-my-pi", 1348), case
def test_parse_issue_ref_rejects_garbage() -> None:
for bad in ("widget#1", "octo/widget", "octo/widget#abc", "octo widget#1", ""):
for bad in (
"widget#1",
"octo/widget",
"octo/widget#abc",
"octo widget#1",
"",
"https://github.com/octo/widget/pull/1",
"https://github.com/octo/widget/issues/",
"https://gitlab.com/octo/widget/issues/1",
):
with pytest.raises(InvalidIssueRef):
parse_issue_ref(bad)
+3 -3
View File
@@ -16,7 +16,7 @@ export function Trigger(): JSX.Element {
const validate = (): string | null => {
const value = issue().trim();
if (!value) return "enter owner/repo#NN";
if (!value) return "enter owner/repo#NN or github issue url";
return null;
};
@@ -33,7 +33,7 @@ export function Trigger(): JSX.Element {
};
return (
<GlassCard heading="trigger" accessory={<span class="text-ink-400">owner/repo#NN</span>}>
<GlassCard heading="trigger" accessory={<span class="text-ink-400">owner/repo#NN or issue url</span>}>
<Show
when={CONFIG.replayEnabled}
fallback={
@@ -48,7 +48,7 @@ export function Trigger(): JSX.Element {
<input
type="text"
spellcheck={false}
placeholder="octo/widget#42"
placeholder="octo/widget#42 or https://github.com/owner/repo/issues/42"
autocomplete="off"
value={issue()}
onInput={(ev) => setIssue(ev.currentTarget.value)}