--- name: 'OSPO Organization Health Report' description: 'Comprehensive weekly health report for a GitHub organization. Surfaces stale issues/PRs, merge time analysis, contributor leaderboards, and actionable items needing human attention.' labels: ['ospo', 'reporting', 'org-health'] on: schedule: - cron: "0 10 * * 1" workflow_dispatch: inputs: organization: description: "GitHub organization to report on" type: string required: true permissions: contents: read issues: read pull-requests: read actions: read engine: copilot tools: github: toolsets: - repos - issues - pull_requests - orgs bash: true safe-outputs: create-issue: max: 1 title-prefix: "[Org Health] " timeout-minutes: 60 network: allowed: - defaults - python --- You are an expert GitHub organization analyst. Your job is to produce a comprehensive weekly health report for your GitHub organization (provided via workflow input). ## Primary Goal **Surface issues and PRs that need human attention**, celebrate wins, and provide actionable metrics so maintainers can prioritize their time. --- ## Step 1 — Determine the Organization ``` ORG = inputs.organization OR "my-org" PERIOD_DAYS = 30 SINCE = date 30 days ago (ISO 8601) STALE_ISSUE_DAYS = 60 STALE_PR_DAYS = 30 60_DAYS_AGO = date 60 days ago (ISO 8601) 30_DAYS_AGO = date 30 days ago (ISO 8601, same as SINCE) ``` ## Step 2 — Gather Organization-Wide Aggregates (Search API) Use GitHub search APIs for fast org-wide counts. These are efficient and avoid per-repo iteration for basic aggregates. Collect the following using search queries: | Metric | Search Query | |--------|-------------| | Total open issues | `org: is:issue is:open` | | Total open PRs | `org: is:pr is:open` | | Issues opened (last 30d) | `org: is:issue created:>={SINCE}` | | Issues closed (last 30d) | `org: is:issue is:closed closed:>={SINCE}` | | PRs opened (last 30d) | `org: is:pr created:>={SINCE}` | | PRs merged (last 30d) | `org: is:pr is:merged merged:>={SINCE}` | | PRs closed unmerged (last 30d) | `org: is:pr is:closed is:unmerged closed:>={SINCE}` | | Stale issues (60+ days) | `org: is:issue is:open updated:<={60_DAYS_AGO}` | | Stale PRs (30+ days) | `org: is:pr is:open updated:<={30_DAYS_AGO}` | **Performance tip:** Add 1–2 second delays between search API calls to stay well within rate limits. ## Step 3 — Stale Issues & PRs (Heat Scores) For stale issues and stale PRs found above, retrieve the top results and sort them by **heat score** (comment count). The heat score helps maintainers prioritize: a stale issue with many comments signals community interest that is going unaddressed. - **Stale issues**: Retrieve up to 50, sort by `comments` descending, keep top 10. For each, record: repo, number, title, days since last update, comment count (heat score), author, labels. - **Stale PRs**: Same approach — retrieve up to 50, sort by `comments` descending, keep top 10. ## Step 4 — PR Merge Time Analysis From the PRs merged in the last 30 days (Step 2), retrieve a sample of recently merged PRs (up to 100). For each, calculate: ``` merge_time = merged_at - created_at (in hours) ``` Then compute percentiles: - **p50** (median merge time) - **p75** - **p95** Use bash with Python for percentile calculations: ```bash python3 -c " import json, sys times = json.loads(sys.stdin.read()) times.sort() n = len(times) if n == 0: print('No data') else: p50 = times[int(n * 0.50)] p75 = times[int(n * 0.75)] p95 = times[int(n * 0.95)] if n >= 20 else times[-1] print(f'p50={p50:.1f}h, p75={p75:.1f}h, p95={p95:.1f}h') " ``` ## Step 5 — First Response Time For issues and PRs opened in the last 30 days, sample up to 50 of each. For each item, find the first comment (excluding the author). Calculate: ``` first_response_time = first_comment.created_at - item.created_at (in hours) ``` Report median first response time for issues and PRs separately. ## Step 6 — Repository Activity & Contributor Leaderboard ### Top 10 Active Repos List all non-archived repos in the org. For each, count pushes / commits / issues+PRs opened in the last 30 days. Sort by total activity, keep top 10. ### Contributor Leaderboard From the top 10 active repos, aggregate commit authors over the last 30 days. Rank by commit count, keep top 10. Award: - 🥇 for #1 - 🥈 for #2 - 🥉 for #3 ### Inactive Repos Repos with 0 pushes, 0 issues, 0 PRs in the last 30 days. List them (name + last push date) so the org can decide whether to archive. ## Step 7 — Health Alerts & Trends Compute velocity indicators and assign status: | Indicator | 🟢 Green | 🟡 Yellow | 🔴 Red | |-----------|----------|-----------|--------| | Issue close rate | closed ≥ opened | closed ≥ 70% opened | closed < 70% opened | | PR merge rate | merged ≥ opened | merged ≥ 60% opened | merged < 60% opened | | Median merge time | < 24h | 24–72h | > 72h | | Median first response | < 24h | 24–72h | > 72h | | Stale issue count | < 10 | 10–50 | > 50 | | Stale PR count | < 5 | 5–20 | > 20 | ## Step 8 — Wins & Shoutouts Celebrate positive signals: - PRs merged with fast turnaround (< 4 hours) - Issues closed quickly (< 24 hours from open to close) - Top contributors (from leaderboard) - Repos with zero stale items ## Step 9 — Compose the Report Create a single issue in the org's `.github` repository (or the most appropriate central repo) with the title: ``` [Org Health] Weekly Report — ``` The issue body should include these sections in order: 1. **Header** — org name, period, generation date 2. **🚨 Health Alerts** — table of indicators with 🟢/🟡/🔴 status and values 3. **🏆 Wins & Shoutouts** — fast merges, quick closes, top contributors 4. **📋 Stale Issues** — top 10 by heat score (repo, issue, days stale, comment count, labels) 5. **📋 Stale PRs** — top 10 by heat score (repo, PR, days stale, comment count, author) 6. **⏱️ PR Merge Time** — p50, p75, p95 percentiles 7. **⚡ First Response Time** — median for issues and PRs 8. **📊 Top 10 Active Repos** — sorted by total activity (issues + PRs + commits) 9. **👥 Contributor Leaderboard** — top 10 by commits with 🥇🥈🥉 10. **😴 Inactive Repos** — repos with 0 activity in 30 days Use markdown tables for all data sections. ## Important Notes - **Update the organization name** in the frontmatter before use. - If any API call fails, note it in the report and continue with available data. Do not let a single failure block the entire report. - Keep the issue body under 65,000 characters (GitHub issue body limit). - All times should be reported in hours. Convert to days only if > 72 hours. - Use the `safe-outputs` constraint: only create 1 issue, with title prefixed `[Org Health] `.