diff --git a/.claude/commands/pr-report.md b/.claude/commands/pr-report.md index 02f83003f18..3a410442b6f 100644 --- a/.claude/commands/pr-report.md +++ b/.claude/commands/pr-report.md @@ -75,6 +75,7 @@ This command generates **two reports** (with two optional additions): | `$OUTPUT_DIR/weekly_pr_report_fast.md` | Data-focused report with metrics and PR listings | | `$OUTPUT_DIR/hypershift_pr_details_fast.json` | Raw PR data in JSON format | | `$OUTPUT_DIR/hypershift_pr_summary.json` | Compact summary data for LLM analysis | +| `$OUTPUT_DIR/weekly_pr_report.html` | Visual HTML dashboard with metrics, charts, and PR listings | | `$OUTPUT_DIR/weekly_pr_report_impact.md` | LLM-generated impact analysis for contributors | | `$OUTPUT_DIR/pr_scored.json` | (--score) Ranked PR list for deep analysis | | `.work/pr_deep/*.json` | (--deep mode) Per-PR data with diffs for analysis | @@ -148,6 +149,15 @@ Read `$OUTPUT_DIR/hypershift_pr_details_fast.json` and `$OUTPUT_DIR/jira_hierarc significant changes, and overall project momentum. Write in a tone suitable for developers and community members following the project.] +## Highlights + +[3-5 bullet points with the most impactful changes this week. Each bullet should be one concise +sentence focused on what shipped and why it matters. These are injected into the HTML dashboard.] + +- [Highlight 1] +- [Highlight 2] +- [Highlight 3] + ## Strategic Initiatives Progress [For each OCPSTRAT initiative with activity this week, provide:] @@ -277,6 +287,25 @@ manual CI work). For each:] | "GCP-216: feat(nodepool): add GCP platform support" | Adds foundational NodePool support for GCP platform, enabling cluster autoscaling and machine management. This is a key milestone for GCP HyperShift availability. | | "CNTRLPLANE-2082: hypershift: run conformance directly on the root cluster" | Simplifies CI architecture by running conformance tests on the management cluster instead of nested clusters. Reduces resource requirements and test complexity. | +**After writing the impact report**, inject the highlights into the HTML dashboard: + +1. Read `$OUTPUT_DIR/weekly_pr_report.html` +2. Take the bullet points from the `## Highlights` section you just wrote +3. Replace `
` with: + +```html +
+

Week Highlights

+ +
+``` + +4. Write the updated HTML back using the Edit tool + ### Step 4: Deep Code Analysis (--deep mode only) If `--deep` flag is specified, perform deep code analysis after the standard report: @@ -698,6 +727,7 @@ After generating reports, provide the user with: 5. (--progress-report mode) Mention the progress report location 6. (--breaking-changes mode) Summarize breaking changes found and their severity 7. (--deep mode) Mention the collaboration report location +8. Location of the HTML dashboard report (`weekly_pr_report.html`) ## Script Features diff --git a/contrib/repo_metrics/weekly_pr_report.py b/contrib/repo_metrics/weekly_pr_report.py index 9879802cc0b..e32d91082e8 100755 --- a/contrib/repo_metrics/weekly_pr_report.py +++ b/contrib/repo_metrics/weekly_pr_report.py @@ -7,6 +7,7 @@ import argparse import asyncio import base64 +import html as html_mod import json import os import re @@ -1194,6 +1195,272 @@ def generate_report(self, output_path: str): print(f"Report written to {output_path}") + def generate_html_report(self, output_path: str): + """Generate a deterministic HTML dashboard report from PR data.""" + e = html_mod.escape + + # --- Compute stats --- + total = len(self.prs) + hs_count = len([p for p in self.prs if p['repo'] == 'openshift/hypershift']) + ai_count = len([p for p in self.prs if p['repo'] == 'openshift-eng/ai-helpers']) + en_count = len([p for p in self.prs if p['repo'] == 'openshift/enhancements']) + rl_count = len([p for p in self.prs if p['repo'] == 'openshift/release']) + + authors = set(p['author'] for p in self.prs) + all_reviewers: Dict[str, int] = {} + for pr in self.prs: + for r in pr.get('reviewers', []): + all_reviewers[r] = all_reviewers.get(r, 0) + 1 + top_reviewers = sorted(all_reviewers.items(), key=lambda x: x[1], reverse=True)[:5] + max_reviews = top_reviewers[0][1] if top_reviewers else 1 + + merge_times = [p['readyToMergeHours'] for p in self.prs if p.get('readyToMergeHours')] + avg_merge = f"{sum(merge_times)/len(merge_times):.1f}" if merge_times else "N/A" + median_merge = f"{sorted(merge_times)[len(merge_times)//2]:.1f}" if merge_times else "N/A" + fastest = min(self.prs, key=lambda p: p.get('readyToMergeHours') or float('inf')) + fastest_h = f"{fastest.get('readyToMergeHours', 0):.1f}" + + merge_days: Dict[str, int] = {} + for pr in self.prs: + day = pr['mergedAt'].split('T')[0] + merge_days[day] = merge_days.get(day, 0) + 1 + sorted_days = sorted(merge_days.items(), key=lambda x: x[1], reverse=True)[:7] + max_day_count = sorted_days[0][1] if sorted_days else 1 + + # Categorise PRs + bug_prs = [] + ai_prs = [] + enhancement_prs = [] + feature_prs = [] + for pr in self.prs: + if pr['repo'] == 'openshift-eng/ai-helpers': + ai_prs.append(pr) + elif pr['repo'] == 'openshift/enhancements': + enhancement_prs.append(pr) + elif any(t.startswith('OCPBUGS') for t in pr.get('jiraTickets', [])): + bug_prs.append(pr) + else: + feature_prs.append(pr) + + bug_prs.sort(key=lambda p: p.get('readyToMergeHours') or 0) + enhancement_prs.sort(key=lambda p: p['mergedAt'], reverse=True) + ai_prs.sort(key=lambda p: p['mergedAt'], reverse=True) + feature_prs.sort(key=lambda p: p['mergedAt'], reverse=True) + + generated = datetime.now(tz=__import__('datetime').timezone.utc).strftime('%Y-%m-%d %H:%M UTC') + + # --- Helper to build a PR table row --- + def _pr_row(pr: Dict, show_repo: bool = False) -> str: + title = e(pr['title'][:90]) + merge_h = f"{pr.get('readyToMergeHours', 0):.1f}h" + repo_col = f"{e(pr['repo'].split('/')[-1])}" if show_repo else "" + return ( + f"" + f"#{pr['number']}" + f"{e(title)}" + f"@{e(pr['author'])}" + f"{repo_col}" + f"{merge_h}" + f"" + ) + + # --- Helper for PR bullet list --- + def _pr_bullet(pr: Dict) -> str: + title = e(pr['title'][:90]) + return ( + f"
  • " + f"#{pr['number']}" + f"{title}" + f"@{e(pr['author'])}" + f"
  • " + ) + + # --- Build merge-day bars --- + day_bars = "" + for day, count in sorted_days: + pct = count / max_day_count * 100 + short_day = day[5:] # MM-DD + day_bars += ( + f"
    " + f"{short_day}" + f"
    " + f"{count} PRs" + f"
    " + ) + + # --- Build reviewer rows --- + reviewer_rows = "" + for reviewer, count in top_reviewers: + bot = " bot" if self.is_bot(reviewer) else "" + bar_w = int(count / max_reviews * 120) + reviewer_rows += ( + f"@{e(reviewer)}{bot}" + f"
    " + f"
    " + f"{count}
    " + ) + + # --- Build bug table rows --- + bug_rows = "".join(_pr_row(pr) for pr in bug_prs) + if not bug_rows: + bug_rows = "No bug fixes this period" + + # --- Build enhancement items --- + enhancement_items = "".join(_pr_bullet(pr) for pr in enhancement_prs) + if not enhancement_items: + enhancement_items = "
  • No enhancements this period
  • " + + # --- Build AI helpers items --- + ai_items = "".join(_pr_bullet(pr) for pr in ai_prs) + if not ai_items: + ai_items = "
  • No ai-helpers PRs this period
  • " + + # --- Build features table rows --- + feature_rows = "".join(_pr_row(pr, show_repo=True) for pr in feature_prs) + if not feature_rows: + feature_rows = "No feature PRs this period" + + # --- Assemble HTML --- + html = f""" + + + + +HyperShift PR Report — {e(self.since_date)} to {e(self.end_date)} + + + + +

    HyperShift PR Report

    +
    {e(self.since_date)} to {e(self.end_date)} · Generated {generated}
    + + +
    +
    {total}
    PRs Merged
    +
    {len(authors)}
    Contributors
    +
    {len(all_reviewers)}
    Reviewers
    +
    {avg_merge}h
    Avg Time to Merge
    Median {median_merge}h · Fastest {fastest_h}h
    +
    + + +
    +

    Repository Breakdown

    +
    +
    {hs_count}
    +
    {rl_count}
    +
    {ai_count}
    +
    {en_count}
    +
    +
    + hypershift ({hs_count}) + release ({rl_count}) + ai-helpers ({ai_count}) + enhancements ({en_count}) +
    +
    + + +
    + + +
    +

    Busiest Merge Days

    {day_bars}
    +

    Top Reviewers

    {reviewer_rows}
    +
    + + +
    +

    Bug Fixes {len(bug_prs)} PRs

    + {bug_rows}
    PRTitleAuthorMerge Time
    +
    + + +
    +

    Enhancement Proposals {len(enhancement_prs)} PRs

    + +
    + + +
    +

    Features & Improvements {len(feature_prs)} PRs

    + {feature_rows}
    PRTitleAuthorRepoMerge Time
    +
    + + +
    +

    AI Helpers {len(ai_prs)} PRs

    + +
    + + + + +""" + + with open(output_path, 'w') as f: + f.write(html) + print(f"HTML report written to {output_path}") + def _get_pr_sfdc_info(self, pr: Dict) -> Tuple[int, int, List[str]]: """Get SFDC case info for a PR from its Jira tickets. @@ -1941,6 +2208,7 @@ async def main(): start_time = time.time() args = parse_args() + since_date = args.since_date end_date = args.end_date deep_prs = args.deep or [] @@ -1971,6 +2239,7 @@ async def main(): generator.generate_report(os.path.join(output_dir, 'weekly_pr_report_fast.md')) generator.save_raw_data(os.path.join(output_dir, 'hypershift_pr_details_fast.json')) generator.save_summary_data(os.path.join(output_dir, 'hypershift_pr_summary.json')) + generator.generate_html_report(os.path.join(output_dir, 'weekly_pr_report.html')) # Score mode: output scored PR list if score_mode: