[TTAHUB-5417] Notifications landing page #40
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}`); | |
| } |