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:
@@ -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"))
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user