mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Improve /pr-status: comments, argument, avoid full log (#89092)
* add general comments * remove full log to avoid the agent accessing it * add PR number argument
This commit is contained in:
@@ -5,17 +5,18 @@ Analyze PR status including CI failures and review comments.
|
||||
## Usage
|
||||
|
||||
```
|
||||
/pr-status
|
||||
/pr-status [PR number]
|
||||
```
|
||||
|
||||
Automatically detects PR from current branch.
|
||||
- Without argument: Automatically detects PR from current branch
|
||||
- With PR number: Analyzes the specified PR (e.g., `/pr-status 89049`)
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Run the script to fetch PR status data:
|
||||
|
||||
```bash
|
||||
node scripts/pr-status.js
|
||||
node scripts/pr-status.js $ARGUMENTS
|
||||
```
|
||||
|
||||
This fetches workflow runs, failed jobs, logs, and PR review comments, then generates markdown files.
|
||||
|
||||
+74
-83
@@ -78,6 +78,11 @@ function stripTimestamps(logContent) {
|
||||
return logContent.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s/gm, '')
|
||||
}
|
||||
|
||||
function isBot(username) {
|
||||
if (!username) return false
|
||||
return username.endsWith('-bot') || username.endsWith('[bot]')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Data Fetching Functions
|
||||
// ============================================================================
|
||||
@@ -223,9 +228,10 @@ function getJobLogs(jobId) {
|
||||
|
||||
function getPRReviews(prNumber) {
|
||||
try {
|
||||
return execJson(
|
||||
const reviews = execJson(
|
||||
`gh api "repos/vercel/next.js/pulls/${prNumber}/reviews" --jq '[.[] | {id, user: .user.login, state: .state, body: .body, submitted_at: .submitted_at, html_url: .html_url}]'`
|
||||
)
|
||||
return reviews.filter((r) => !isBot(r.user))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
@@ -270,9 +276,10 @@ function getPRReviewThreads(prNumber) {
|
||||
|
||||
function getPRComments(prNumber) {
|
||||
try {
|
||||
return execJson(
|
||||
const comments = execJson(
|
||||
`gh api "repos/vercel/next.js/issues/${prNumber}/comments" --jq '[.[] | {id, user: .user.login, body: .body, created_at: .created_at, html_url: .html_url}]'`
|
||||
)
|
||||
return comments.filter((c) => !isBot(c.user))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
@@ -542,9 +549,9 @@ function generateIndexMd(
|
||||
'|----------|-------|-----------|---------|'
|
||||
)
|
||||
|
||||
// Sort reviews by date, most recent first
|
||||
// Sort reviews by date, oldest first
|
||||
const sortedReviews = [...meaningfulReviews].sort(
|
||||
(a, b) => new Date(b.submitted_at) - new Date(a.submitted_at)
|
||||
(a, b) => new Date(a.submitted_at) - new Date(b.submitted_at)
|
||||
)
|
||||
|
||||
for (const review of sortedReviews) {
|
||||
@@ -583,6 +590,33 @@ function generateIndexMd(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// General comments section
|
||||
if (prComments.length > 0) {
|
||||
lines.push(
|
||||
'',
|
||||
`## General Comments (${prComments.length})`,
|
||||
'',
|
||||
'| Author | Date/Time | Details |',
|
||||
'|--------|-----------|---------|'
|
||||
)
|
||||
|
||||
const sortedComments = [...prComments].sort(
|
||||
(a, b) => new Date(a.created_at) - new Date(b.created_at)
|
||||
)
|
||||
|
||||
for (const comment of sortedComments) {
|
||||
const time = comment.created_at
|
||||
? new Date(comment.created_at)
|
||||
.toISOString()
|
||||
.replace('T', ' ')
|
||||
.substring(0, 19)
|
||||
: 'N/A'
|
||||
lines.push(
|
||||
`| ${escapeMarkdownTableCell(comment.user)} | ${time} | [View](comment-${comment.id}.md) |`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
@@ -603,7 +637,6 @@ function generateJobMd(jobMetadata, testResults, testGroups, sections) {
|
||||
`Completed: ${jobMetadata.completed_at}`,
|
||||
`Duration: ${duration}`,
|
||||
`URL: ${jobMetadata.html_url}`,
|
||||
`Full Log: [job-${jobMetadata.id}-full-log.txt](job-${jobMetadata.id}-full-log.txt)`,
|
||||
'',
|
||||
]
|
||||
|
||||
@@ -733,73 +766,6 @@ function generateTestMd(jobMetadata, testPath, content, testResultJson) {
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function generateReviewsMd(reviews, prComments) {
|
||||
const lines = ['# PR Reviews', '']
|
||||
|
||||
if (reviews.length === 0 && prComments.length === 0) {
|
||||
lines.push('No reviews or comments found.')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
// Sort reviews by submitted_at date
|
||||
const sortedReviews = [...reviews].sort(
|
||||
(a, b) => new Date(b.submitted_at) - new Date(a.submitted_at)
|
||||
)
|
||||
|
||||
if (sortedReviews.length > 0) {
|
||||
lines.push('## Reviews', '')
|
||||
|
||||
for (const review of sortedReviews) {
|
||||
const date = review.submitted_at
|
||||
? new Date(review.submitted_at).toISOString().split('T')[0]
|
||||
: 'N/A'
|
||||
const stateEmoji =
|
||||
review.state === 'APPROVED'
|
||||
? '[APPROVED]'
|
||||
: review.state === 'CHANGES_REQUESTED'
|
||||
? '[CHANGES_REQUESTED]'
|
||||
: review.state === 'COMMENTED'
|
||||
? '[COMMENTED]'
|
||||
: `[${review.state}]`
|
||||
|
||||
lines.push(`### ${stateEmoji} ${review.user} - ${date}`, '')
|
||||
|
||||
if (review.body && review.body.trim()) {
|
||||
lines.push(review.body.trim(), '')
|
||||
} else {
|
||||
lines.push('_No comment body_', '')
|
||||
}
|
||||
|
||||
lines.push(`[View on GitHub](${review.html_url})`, '', '---', '')
|
||||
}
|
||||
}
|
||||
|
||||
// Add general PR comments (issue-style comments)
|
||||
if (prComments.length > 0) {
|
||||
lines.push('## General Comments', '')
|
||||
|
||||
const sortedComments = [...prComments].sort(
|
||||
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||
)
|
||||
|
||||
for (const comment of sortedComments) {
|
||||
const date = comment.created_at
|
||||
? new Date(comment.created_at).toISOString().split('T')[0]
|
||||
: 'N/A'
|
||||
|
||||
lines.push(`### ${comment.user} - ${date}`, '')
|
||||
|
||||
if (comment.body?.trim()) {
|
||||
lines.push(comment.body.trim(), '')
|
||||
}
|
||||
|
||||
lines.push(`[View on GitHub](${comment.html_url})`, '', '---', '')
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function generateReviewMd(review) {
|
||||
const time = review.submitted_at
|
||||
? new Date(review.submitted_at)
|
||||
@@ -822,6 +788,28 @@ function generateReviewMd(review) {
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function generateCommentMd(comment) {
|
||||
const time = comment.created_at
|
||||
? new Date(comment.created_at)
|
||||
.toISOString()
|
||||
.replace('T', ' ')
|
||||
.substring(0, 19)
|
||||
: 'N/A'
|
||||
|
||||
const lines = [
|
||||
`# Comment by ${comment.user}`,
|
||||
'',
|
||||
`Time: ${time}`,
|
||||
`URL: ${comment.html_url}`,
|
||||
'',
|
||||
'## Comment',
|
||||
'',
|
||||
comment.body?.trim() || '_No content_',
|
||||
]
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function generateThreadMd(thread, index) {
|
||||
const lines = [
|
||||
`# Thread ${index + 1}: ${thread.path}`,
|
||||
@@ -943,10 +931,6 @@ async function main() {
|
||||
|
||||
// Write review files if we have PR data
|
||||
if (reviewData) {
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, 'reviews.md'),
|
||||
generateReviewsMd(reviewData.reviews, reviewData.prComments)
|
||||
)
|
||||
// Write individual thread files
|
||||
for (let i = 0; i < reviewData.reviewThreads.length; i++) {
|
||||
const thread = reviewData.reviewThreads[i]
|
||||
@@ -964,6 +948,13 @@ async function main() {
|
||||
)
|
||||
}
|
||||
}
|
||||
// Write individual comment files
|
||||
for (const comment of reviewData.prComments) {
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, `comment-${comment.id}.md`),
|
||||
generateCommentMd(comment)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const emptyCategorizedJobs = {
|
||||
@@ -1008,9 +999,6 @@ async function main() {
|
||||
// Get job logs
|
||||
const logs = getJobLogs(id)
|
||||
|
||||
// Write full log
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, `job-${id}-full-log.txt`), logs)
|
||||
|
||||
// Extract test output JSON
|
||||
const testResults = extractTestOutputJson(logs)
|
||||
|
||||
@@ -1068,10 +1056,6 @@ async function main() {
|
||||
// Step 7: Write PR review files if we have PR data
|
||||
if (reviewData) {
|
||||
console.log('Generating review files...')
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, 'reviews.md'),
|
||||
generateReviewsMd(reviewData.reviews, reviewData.prComments)
|
||||
)
|
||||
// Write individual thread files
|
||||
for (let i = 0; i < reviewData.reviewThreads.length; i++) {
|
||||
const thread = reviewData.reviewThreads[i]
|
||||
@@ -1089,6 +1073,13 @@ async function main() {
|
||||
)
|
||||
}
|
||||
}
|
||||
// Write individual comment files
|
||||
for (const comment of reviewData.prComments) {
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, `comment-${comment.id}.md`),
|
||||
generateCommentMd(comment)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: Generate index.md
|
||||
|
||||
Reference in New Issue
Block a user