|
18 | 18 |
|
19 | 19 |
|
20 | 20 | USER_AGENT = "dotnet-skills-upstream-watch" |
| 21 | +ISSUE_TITLE_PREFIX = "Upstream update: " |
| 22 | +MAX_ISSUE_TITLE_LENGTH = 256 |
21 | 23 | MARKER_RE = re.compile(r"<!-- upstream-watch:id=(?P<watch_id>[^>]+) -->") |
22 | 24 | VALUE_MARKER_RE = re.compile(r"<!-- upstream-watch:value=(?P<value>[^>]+) -->") |
23 | 25 | ISSUE_KEY_MARKER_RE = re.compile(r"<!-- upstream-watch:issue-key=(?P<issue_key>[^>]+) -->") |
@@ -223,6 +225,15 @@ def default_issue_name(skills: list[str]) -> str: |
223 | 225 | return " + ".join(ordered) |
224 | 226 |
|
225 | 227 |
|
| 228 | +def condensed_issue_name(skills: list[str]) -> str | None: |
| 229 | + ordered = sorted(dict.fromkeys(skills)) |
| 230 | + if not ordered: |
| 231 | + return None |
| 232 | + if len(ordered) <= 3: |
| 233 | + return " + ".join(ordered) |
| 234 | + return f"{ordered[0]} + {ordered[1]} + {ordered[2]} + {len(ordered) - 3} more" |
| 235 | + |
| 236 | + |
226 | 237 | def validate_issue_group_fields(normalized: dict[str, Any], raw_watch: dict[str, Any]) -> None: |
227 | 238 | issue_key = raw_watch.get("issue_key") or default_issue_key(normalized["skills"]) |
228 | 239 | issue_name = raw_watch.get("issue_name") or default_issue_name(normalized["skills"]) |
@@ -584,7 +595,55 @@ def parse_open_issue( |
584 | 595 |
|
585 | 596 |
|
586 | 597 | def issue_title(issue_name: str) -> str: |
587 | | - return f"Upstream update: {issue_name}" |
| 598 | + return f"{ISSUE_TITLE_PREFIX}{issue_name}" |
| 599 | + |
| 600 | + |
| 601 | +def parse_issue_name_from_title(title: str | None) -> str | None: |
| 602 | + if not isinstance(title, str): |
| 603 | + return None |
| 604 | + if not title.startswith(ISSUE_TITLE_PREFIX): |
| 605 | + return None |
| 606 | + issue_name = title[len(ISSUE_TITLE_PREFIX) :].strip() |
| 607 | + return issue_name or None |
| 608 | + |
| 609 | + |
| 610 | +def truncate_issue_name(issue_name: str) -> str: |
| 611 | + max_issue_name_length = MAX_ISSUE_TITLE_LENGTH - len(ISSUE_TITLE_PREFIX) |
| 612 | + if max_issue_name_length <= 0 or len(issue_name) <= max_issue_name_length: |
| 613 | + return issue_name |
| 614 | + if max_issue_name_length <= 3: |
| 615 | + return issue_name[:max_issue_name_length] |
| 616 | + return issue_name[: max_issue_name_length - 3].rstrip() + "..." |
| 617 | + |
| 618 | + |
| 619 | +def resolve_issue_name( |
| 620 | + *, |
| 621 | + issue_key: str, |
| 622 | + skills: list[str], |
| 623 | + configured_issue_name: str | None = None, |
| 624 | + existing_issue_name: str | None = None, |
| 625 | +) -> str: |
| 626 | + candidates: list[str] = [] |
| 627 | + for candidate in ( |
| 628 | + configured_issue_name, |
| 629 | + existing_issue_name, |
| 630 | + default_issue_name(skills) if skills else None, |
| 631 | + condensed_issue_name(skills), |
| 632 | + issue_key, |
| 633 | + ): |
| 634 | + if not isinstance(candidate, str): |
| 635 | + continue |
| 636 | + cleaned = candidate.strip() |
| 637 | + if not cleaned or cleaned in candidates: |
| 638 | + continue |
| 639 | + candidates.append(cleaned) |
| 640 | + |
| 641 | + for candidate in candidates: |
| 642 | + if len(issue_title(candidate)) <= MAX_ISSUE_TITLE_LENGTH: |
| 643 | + return candidate |
| 644 | + |
| 645 | + fallback = candidates[-1] if candidates else issue_key.strip() or "upstream-watch" |
| 646 | + return truncate_issue_name(fallback) |
588 | 647 |
|
589 | 648 |
|
590 | 649 | def issue_body( |
@@ -757,10 +816,14 @@ def load_open_issue_groups( |
757 | 816 | "issues": [], |
758 | 817 | "pending_watches": {}, |
759 | 818 | "skills": [], |
| 819 | + "issue_name": None, |
760 | 820 | "fresh": False, |
761 | 821 | }, |
762 | 822 | ) |
763 | 823 | group["issues"].append(issue) |
| 824 | + issue_name = parse_issue_name_from_title(issue.get("title")) |
| 825 | + if issue_name and not group.get("issue_name"): |
| 826 | + group["issue_name"] = issue_name |
764 | 827 |
|
765 | 828 | for skill in skills: |
766 | 829 | if skill not in group["skills"]: |
@@ -842,7 +905,11 @@ def reconcile_open_issues( |
842 | 905 | canonical_issue = choose_canonical_issue(group["issues"]) |
843 | 906 | pending_watches = group["pending_watches"] |
844 | 907 | skills = collect_group_skills(pending_watches, watch_index, fallback_skills=group.get("skills")) |
845 | | - issue_name = default_issue_name(skills) if skills else issue_key |
| 908 | + issue_name = resolve_issue_name( |
| 909 | + issue_key=issue_key, |
| 910 | + skills=skills, |
| 911 | + existing_issue_name=group.get("issue_name"), |
| 912 | + ) |
846 | 913 | title = issue_title(issue_name) |
847 | 914 | body = issue_body( |
848 | 915 | issue_key=issue_key, |
@@ -911,7 +978,12 @@ def rotate_issue( |
911 | 978 | existing_watch_snapshot = pending_watches.get(watch["id"]) |
912 | 979 | pending_watches[watch["id"]] = minimal_snapshot(new_snapshot) |
913 | 980 | skills = collect_group_skills(pending_watches, watch_index, fallback_skills=watch.get("skills")) |
914 | | - issue_name = default_issue_name(skills) if skills else watch.get("issue_name", issue_key) |
| 981 | + issue_name = resolve_issue_name( |
| 982 | + issue_key=issue_key, |
| 983 | + skills=skills, |
| 984 | + configured_issue_name=watch.get("issue_name"), |
| 985 | + existing_issue_name=existing_group.get("issue_name") if existing_group else None, |
| 986 | + ) |
915 | 987 | title = issue_title(issue_name) |
916 | 988 | body = issue_body( |
917 | 989 | issue_key=issue_key, |
|
0 commit comments