Skip to content

Commit 0aad742

Browse files
More detail for stale test results. More consistent formatting
1 parent 9788537 commit 0aad742

2 files changed

Lines changed: 166 additions & 25 deletions

File tree

.claude/commands/branch-status.md

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,24 @@ Parse the JSON and produce a report with two sections.
4242

4343
### GitHub PRs
4444

45-
For each repo with a PR, show: repo name, PR title, approval state (N approved / M changes requested / K pending reviewers), CI rollup, draft status, and mergeability. Group repos with no PR separately.
45+
For each repo with a PR, show: repo name, PR title, approval state (N approved / M changes requested / K pending reviewers), CI rollup, draft status, and a "Ready" column. Group repos with no PR separately.
4646

47-
Call out blockers explicitly: PRs needing approvals, PRs with changes requested, CI failures, draft PRs that need to be un-drafted, and merge conflicts.
47+
The "Ready" column reflects whether this PR is ready to merge, combining GitHub mergeability and task list completion:
48+
- "Yes" — GitHub says MERGEABLE and no unchecked blocker tasks (or no task list)
49+
- "Tasks pending" — GitHub says MERGEABLE but has unchecked blocker tasks
50+
- "Conflicts" — GitHub says CONFLICTING (merge conflict), regardless of tasks
51+
- "Unknown" — GitHub merge state is unknown
52+
53+
For the CI column, use title case: Success, Failure, Pending — not all caps.
54+
55+
Call out blockers explicitly: PRs needing approvals, PRs with changes requested, CI failures, draft PRs that need to be un-drafted, merge conflicts, and unchecked blocker tasks.
56+
57+
For each PR that has task list items (from `task_items` in the JSON) or linked issues with task items (from `linked_issues[].task_items`), list unchecked items grouped by whether they are blockers or deferrable. Show PR task items and linked issue task items together, labelling the source when both are present (e.g. "PR task list" vs "Issue #123 task list").
58+
59+
- **Deferrable** (OK to be incomplete at merge time): "TeamCity verify and merge" or similar CI-trigger tasks; "user education handoff", "customer comms", "documentation handoff", or any post-merge communication/education step.
60+
- **Blockers** (must be complete before merge): "manual test", "manual testing", "automated test", "write test/tests", "QA", or any other pre-merge validation step. When in doubt, treat an unchecked item as a blocker.
61+
62+
Note: LabKey PRs are often approved during code review, which may happen before testing is complete. An approval does not imply testing is done — check the task list explicitly.
4863

4964
### TeamCity Builds
5065

@@ -62,10 +77,71 @@ Group suites into four categories (show non-empty categories only):
6277
- **New failures** (`fails_on_primary: false`) — require immediate attention
6378
- **Pre-existing failures** (`fails_on_primary: true`) — not caused by this branch
6479
- Tests with unknown primary-branch status
65-
- Note if `has_newer_commits: true` — result may be outdated
66-
3. **Passing** (`status: SUCCESS`, `state: finished`) — list with a stale warning if `has_newer_commits: true`
67-
4. **Not Yet Triggered** (`status: NOT_STARTED`) — suites in the same sub-projects as known builds that have never run on this branch; list as a count with names
80+
- If `has_newer_commits: true`, note which repos had newer commits (from `stale_repos`) — result may be outdated
81+
3. **Passing** (`status: SUCCESS`, `state: finished`) — list with a stale note if `has_newer_commits: true`; for stale builds include which repos had newer commits (from `stale_repos`)
82+
4. **Not Yet Triggered** (`status: NOT_STARTED`) — suites in the same sub-projects as known builds that have never run on this branch; show only the count, not the individual names
83+
84+
### Overall Assessment
85+
86+
End with a one-paragraph verdict: is this branch ready to merge? Factor in: PR approvals, CI/TC results (and whether they are current or stale), and PR task list completion. Call out any unchecked blocker tasks (manual test, automated test, etc.) as merge blockers even if the PR is already approved. Deferrable tasks (TeamCity verify and merge, user education handoff, etc.) should not block the verdict. If not ready, state specifically what needs to happen first.
87+
88+
---
89+
90+
## Example Output
91+
92+
### GitHub PRs
93+
94+
| Repo | PR | Approvals | CI | Ready |
95+
|---|---|---|---|---|
96+
| LabKey/platform | [#7673 — Prevent popup widget from wrapping](url) | 1 approved | Success | Tasks pending |
97+
| LabKey/targetedms | [#1208 — flat protein/molecule list](url) | 1 approved | Success | Yes |
98+
| LabKey/testAutomation | [#3003 — Remove unused deprecated method](url) | 1 approved | Success | Yes |
99+
100+
**LabKey/platform task list:**
101+
- [x] Code review
102+
- [x] Manual test
103+
- [ ] TeamCity verify and merge *(deferrable)*
104+
105+
**LabKey/targetedms task list:**
106+
- [x] Code review
107+
- [ ] Manual test *(blocker)*
108+
- [ ] User education handoff *(deferrable)*
109+
110+
Blockers: LabKey/targetedms — manual test not yet checked off.
111+
112+
---
113+
114+
### TeamCity Builds
115+
116+
**Failures** (1)
117+
118+
- MS2 sqlserver — 1 new failure (STALE: newer commits exist since build)
119+
- org.labkey.test.tests.ms2.CometTest.testSteps
120+
121+
**Passing** — current (queued after latest commit 2026-05-18 21:07)
122+
123+
- Panorama [A] postgres
124+
- Panorama [B] postgres
125+
- Upgrade from 25.11
126+
- Upgrade from 25.7
127+
- Upgrade setup
128+
- Upgrade validation
129+
- build (Premium)
130+
131+
**Passing** — stale (queued before latest commit)
132+
133+
- BVT-EHR postgres
134+
- BVT-EHR sqlserver
135+
- MS2 Postgres
136+
- Verify Java Build
137+
- Verify Test Build
138+
- build (Community)
139+
- build_ehr
140+
141+
**Not Yet Triggered** (71 suites)
142+
143+
---
68144

69145
### Overall Assessment
70146

71-
End with a one-paragraph verdict: is this branch ready to merge? Factor in whether the shown results are current (`has_newer_commits`) or whether builds are still in progress. If not ready, state specifically what needs to happen first.
147+
Not ready to merge. The LabKey/targetedms PR has an unchecked manual test task. The MS2 sqlserver suite also shows a new failure (CometTest) though that build is stale — it should be re-run after the latest commits before merging.

.claude/scripts/branch-status.py

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,30 @@ def get_topic_repos() -> list[str]:
103103
# GitHub PR status
104104
# ---------------------------------------------------------------------------
105105

106+
@dataclass
107+
class TaskItem:
108+
text: str
109+
checked: bool
110+
111+
112+
def _parse_task_list(body: str) -> list[TaskItem]:
113+
"""Parse GitHub-flavored markdown task list items from a PR body."""
114+
items = []
115+
for line in (body or "").splitlines():
116+
m = re.match(r'^\s*[-*]\s+\[([ xX])\]\s+(.*)', line)
117+
if m:
118+
items.append(TaskItem(text=m.group(2).strip(), checked=m.group(1).lower() == 'x'))
119+
return items
120+
121+
122+
@dataclass
123+
class LinkedIssue:
124+
number: int
125+
title: str
126+
url: str
127+
task_items: list[TaskItem] = field(default_factory=list)
128+
129+
106130
@dataclass
107131
class PRStatus:
108132
repo: str
@@ -115,6 +139,8 @@ class PRStatus:
115139
pending_reviewers: list[str] = field(default_factory=list)
116140
mergeable: str = "" # MERGEABLE CONFLICTING UNKNOWN
117141
ci_rollup: str = "" # SUCCESS FAILURE PENDING ""
142+
task_items: list[TaskItem] = field(default_factory=list)
143+
linked_issues: list[LinkedIssue] = field(default_factory=list)
118144

119145

120146
def _ci_rollup(checks: list[dict]) -> str:
@@ -140,7 +166,7 @@ def fetch_pr_status(owner_repo: str, branch: str) -> PRStatus:
140166
"--repo", owner_repo,
141167
"--head", branch,
142168
"--state", "all",
143-
"--json", "title,url,state,isDraft,reviews,reviewRequests,mergeable,statusCheckRollup",
169+
"--json", "title,url,state,isDraft,reviews,reviewRequests,mergeable,statusCheckRollup,body,closingIssuesReferences",
144170
"--limit", "1",
145171
)
146172
if not ok or not stdout:
@@ -161,6 +187,22 @@ def fetch_pr_status(owner_repo: str, branch: str) -> PRStatus:
161187
ps.is_draft = pr.get("isDraft", False)
162188
ps.mergeable = pr.get("mergeable", "").upper()
163189
ps.ci_rollup = _ci_rollup(pr.get("statusCheckRollup", []))
190+
ps.task_items = _parse_task_list(pr.get("body", "") or "")
191+
192+
for ref in pr.get("closingIssuesReferences", []):
193+
num = ref.get("number", 0)
194+
if not num:
195+
continue
196+
body_out, ok = run(
197+
"gh", "api", f"repos/{owner_repo}/issues/{num}",
198+
"--jq", ".body // empty",
199+
)
200+
ps.linked_issues.append(LinkedIssue(
201+
number=num,
202+
title=ref.get("title", ""),
203+
url=ref.get("url", ""),
204+
task_items=_parse_task_list(body_out if ok else ""),
205+
))
164206

165207
# Use the latest review state per reviewer (last review wins).
166208
latest_by_reviewer: dict[str, str] = {}
@@ -433,6 +475,7 @@ class BuildStatus:
433475
failure_count: int = 0
434476
failed_tests: list[FailedTest] = field(default_factory=list)
435477
has_newer_commits: Optional[bool] = None # True = branch has commits newer than this build's queue time
478+
stale_repos: list[str] = field(default_factory=list) # repos with commits newer than build queue time
436479

437480

438481
def _fetch_failing_tests(build_id: int, token: str, max_tests: int = 500) -> list[str]:
@@ -528,14 +571,13 @@ def _fetch_repo_latest_commit_date(owner_repo: str, branch: str) -> Optional[str
528571
return stdout.strip() if ok and stdout.strip() else None
529572

530573

531-
def _get_latest_branch_commit_date(repos: list[str], branch: str) -> Optional[str]:
532-
"""Return the most recent commit date (ISO 8601) across all given repos, or None."""
574+
def _get_repo_commit_dates(repos: list[str], branch: str) -> dict[str, str]:
575+
"""Return {owner_repo: ISO date} for the latest commit on each repo's branch."""
533576
if not repos:
534-
return None
577+
return {}
535578
with ThreadPoolExecutor(max_workers=8) as ex:
536-
futures = [ex.submit(_fetch_repo_latest_commit_date, r, branch) for r in repos]
537-
dates = [f.result() for f in futures if f.result()]
538-
return max(dates) if dates else None
579+
futures = {r: ex.submit(_fetch_repo_latest_commit_date, r, branch) for r in repos}
580+
return {r: f.result() for r, f in futures.items() if f.result()}
539581

540582

541583
def _fetch_all_build_type_ids(project_id: str, token: str) -> list[tuple[str, str, str]]:
@@ -761,6 +803,22 @@ def to_dict(
761803
"pending_reviewers": ps.pending_reviewers,
762804
"mergeable": ps.mergeable,
763805
"ci_rollup": ps.ci_rollup,
806+
"task_items": [
807+
{"text": ti.text, "checked": ti.checked}
808+
for ti in ps.task_items
809+
],
810+
"linked_issues": [
811+
{
812+
"number": li.number,
813+
"title": li.title,
814+
"url": li.url,
815+
"task_items": [
816+
{"text": ti.text, "checked": ti.checked}
817+
for ti in li.task_items
818+
],
819+
}
820+
for li in ps.linked_issues
821+
],
764822
}
765823
for ps in sorted(github, key=lambda x: x.repo)
766824
],
@@ -775,6 +833,7 @@ def to_dict(
775833
"finished_at": bs.finished_at,
776834
"queued_at": _parse_tc_date(bs.queued_at) if bs.queued_at else "",
777835
"has_newer_commits": bs.has_newer_commits,
836+
"stale_repos": bs.stale_repos,
778837
"failure_count": bs.failure_count,
779838
"failed_tests": [
780839
{"name": ft.name, "fails_on_primary": ft.fails_on_primary}
@@ -846,19 +905,25 @@ def main() -> None:
846905
github = github_future.result()
847906
builds = tc_future.result()
848907

849-
# Mark stale builds: has the branch received commits since this build was queued?
908+
# Mark stale builds: which repos have commits newer than each build's queue time?
850909
repos_with_branch = [ps.repo for ps in github if ps.state != "NO_PR"]
851-
latest_commit_date = _get_latest_branch_commit_date(repos_with_branch, args.branch)
852-
if latest_commit_date:
853-
try:
854-
commit_dt = datetime.fromisoformat(latest_commit_date.replace("Z", "+00:00"))
855-
for bs in builds:
856-
if bs.state == "finished" and bs.queued_at:
857-
queued_dt = _parse_tc_date_to_dt(bs.queued_at)
858-
if queued_dt:
859-
bs.has_newer_commits = commit_dt > queued_dt
860-
except ValueError:
861-
pass
910+
repo_commit_dates = _get_repo_commit_dates(repos_with_branch, args.branch)
911+
latest_commit_date = max(repo_commit_dates.values()) if repo_commit_dates else None
912+
913+
if repo_commit_dates:
914+
for bs in builds:
915+
if bs.state == "finished" and bs.queued_at:
916+
queued_dt = _parse_tc_date_to_dt(bs.queued_at)
917+
if queued_dt:
918+
stale = []
919+
for repo, date_str in repo_commit_dates.items():
920+
try:
921+
if datetime.fromisoformat(date_str.replace("Z", "+00:00")) > queued_dt:
922+
stale.append(repo)
923+
except ValueError:
924+
pass
925+
bs.stale_repos = sorted(stale)
926+
bs.has_newer_commits = bool(stale)
862927

863928
if args.as_json:
864929
print(json.dumps(to_dict(args.branch, tc_branch, primary_branch, github, builds, latest_commit_date), indent=2))

0 commit comments

Comments
 (0)