mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-12 04:05:12 +00:00
217 lines
6.8 KiB
Markdown
217 lines
6.8 KiB
Markdown
---
|
||
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:<ORG> is:issue is:open` |
|
||
| Total open PRs | `org:<ORG> is:pr is:open` |
|
||
| Issues opened (last 30d) | `org:<ORG> is:issue created:>={SINCE}` |
|
||
| Issues closed (last 30d) | `org:<ORG> is:issue is:closed closed:>={SINCE}` |
|
||
| PRs opened (last 30d) | `org:<ORG> is:pr created:>={SINCE}` |
|
||
| PRs merged (last 30d) | `org:<ORG> is:pr is:merged merged:>={SINCE}` |
|
||
| PRs closed unmerged (last 30d) | `org:<ORG> is:pr is:closed is:unmerged closed:>={SINCE}` |
|
||
| Stale issues (60+ days) | `org:<ORG> is:issue is:open updated:<={60_DAYS_AGO}` |
|
||
| Stale PRs (30+ days) | `org:<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 — <DATE>
|
||
```
|
||
|
||
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] `.
|