Skip to content

[TTAHUB-5417] Notifications landing page #40

[TTAHUB-5417] Notifications landing page

[TTAHUB-5417] Notifications landing page #40

name: PR Review Metrics Comment
on:
pull_request:
types: [closed]
permissions:
contents: read
pull-requests: read
issues: write
concurrency:
group: pr-review-metrics-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
post_review_metrics:
if: github.event.pull_request.base.ref != 'production'
runs-on: ubuntu-latest
steps:
- name: Compute and post review metrics
uses: actions/github-script@v8
with:
script: |
try {
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request.number;
const pr = context.payload.pull_request;
const isBotAccount = (login) => {
if (!login) return true;
if (login.endsWith('[bot]')) return true;
const knownBots = ['dependabot', 'github-actions', 'copilot', 'renovate', 'snyk-bot'];
return knownBots.some((b) => login.toLowerCase().startsWith(b));
};
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner, repo, pull_number: prNumber, per_page: 100,
});
const humanReviews = reviews.filter(
(r) => !isBotAccount(r.user?.login) && r.user?.login !== pr.user.login,
);
const uniqueReviewers = [...new Set(humanReviews.map((r) => r.user?.login).filter(Boolean))];
// First-review turnaround: ready_for_review event or pr.created_at
let readyAt = new Date(pr.created_at);
const timelineEvents = await github.paginate(github.rest.issues.listEventsForTimeline, {
owner, repo, issue_number: prNumber, per_page: 100,
});
const readyEvent = timelineEvents.find((e) => e.event === 'ready_for_review');
if (readyEvent?.created_at) readyAt = new Date(readyEvent.created_at);
const reviewsSortedByDate = [...humanReviews].sort(
(a, b) => new Date(a.submitted_at) - new Date(b.submitted_at),
);
let firstReviewTurnaroundStr = '—';
if (reviewsSortedByDate.length > 0) {
const hours = Math.max(0, (new Date(reviewsSortedByDate[0].submitted_at) - readyAt) / 36e5);
firstReviewTurnaroundStr = hours < 1 ? `${Math.round(hours * 60)} min` : `${hours.toFixed(1)} hr`;
}
const stateEmoji = { APPROVED: '✅', CHANGES_REQUESTED: '🔁', COMMENTED: '💬', DISMISSED: '🚫' };
const timelineRows = reviewsSortedByDate.map((r) => {
const emoji = stateEmoji[r.state] || '❓';
const when = new Date(r.submitted_at).toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
return `| ${emoji} ${r.state} | @${r.user?.login ?? 'unknown'} | ${when} |`;
});
const prStatus = pr.merged ? '✅ Merged' : '🚫 Closed (not merged)';
const reviewerList = uniqueReviewers.length
? uniqueReviewers.map((u) => `@${u}`).join(', ')
: '_No human reviews recorded_';
const timelineSection = timelineRows.length
? `\n\n### Review Timeline\n\n| State | Reviewer | Submitted At |\n|-------|----------|-------------|\n${timelineRows.join('\n')}`
: '';
const body = [
'## 📊 Review Metrics',
'',
`> Auto-generated by the [PR Review Metrics workflow](${context.serverUrl}/${owner}/${repo}/actions/workflows/pr-review-metrics.yml). Observe-only — no action required.`,
'',
'| Metric | Value |',
'|--------|-------|',
`| PR Status | ${prStatus} |`,
`| Reviewer Count | ${uniqueReviewers.length} |`,
`| Reviewers | ${reviewerList} |`,
`| First-Review Turnaround | ${firstReviewTurnaroundStr} |`,
`| Total Review Events | ${humanReviews.length} |`,
timelineSection,
].join('\n');
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
core.info(`Posted review metrics on PR #${prNumber}: ${uniqueReviewers.length} reviewers, ${firstReviewTurnaroundStr}`);
} catch (e) {
core.warning(`post_review_metrics failed: ${e.message}`);
}