mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 12:55:06 +03:00
227 lines
7.2 KiB
JavaScript
227 lines
7.2 KiB
JavaScript
/**
|
|
* Finds stale Dashboard PRs (older than 24 hours) and fetches their status
|
|
* including review status and mergeable state.
|
|
*
|
|
* @param {Object} github - GitHub API client from actions/github-script
|
|
* @param {Object} context - GitHub Actions context
|
|
* @param {Object} core - GitHub Actions core utilities
|
|
* @returns {Array} Array of stale PRs with status information
|
|
*/
|
|
module.exports = async ({ github, context, core }) => {
|
|
const TWENTY_FOUR_HOURS_AGO = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
|
const DASHBOARD_PATH = 'apps/studio/'
|
|
|
|
console.log(`Looking for PRs older than: ${TWENTY_FOUR_HOURS_AGO.toISOString()}`)
|
|
|
|
const stalePRs = []
|
|
let page = 1
|
|
let hasMore = true
|
|
|
|
// Fetch PRs page by page, newest first
|
|
while (hasMore && page <= 10) {
|
|
// Limit to 10 pages (1000 PRs) as safety measure
|
|
console.log(`Fetching page ${page}...`)
|
|
|
|
const { data: prs } = await github.rest.pulls.list({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
state: 'open',
|
|
sort: 'created',
|
|
direction: 'desc',
|
|
per_page: 100,
|
|
page: page,
|
|
})
|
|
|
|
if (prs.length === 0) {
|
|
hasMore = false
|
|
break
|
|
}
|
|
|
|
// Check each PR
|
|
for (const pr of prs) {
|
|
// Skip PRs from forks - only check internal PRs
|
|
if (pr.head.repo && pr.head.repo.full_name !== context.repo.owner + '/' + context.repo.repo) {
|
|
console.log(`PR #${pr.number} is from a fork, skipping...`)
|
|
continue
|
|
}
|
|
|
|
// Skip dependabot PRs
|
|
if (pr.user.login === 'dependabot[bot]' || pr.user.login === 'dependabot') {
|
|
console.log(`PR #${pr.number} is from dependabot, skipping...`)
|
|
continue
|
|
}
|
|
|
|
// Skip draft PRs
|
|
if (pr.draft) {
|
|
console.log(`PR #${pr.number} is a draft, skipping...`)
|
|
continue
|
|
}
|
|
|
|
const createdAt = new Date(pr.created_at)
|
|
|
|
// If this PR is newer than 24 hours, skip it
|
|
if (createdAt > TWENTY_FOUR_HOURS_AGO) {
|
|
console.log(`PR #${pr.number} is too new, skipping...`)
|
|
continue
|
|
}
|
|
|
|
console.log(`Checking PR #${pr.number}: ${pr.title}`)
|
|
|
|
// Fetch files changed in this PR
|
|
const { data: files } = await github.rest.pulls.listFiles({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: pr.number,
|
|
per_page: 100,
|
|
})
|
|
|
|
// Check if any file is under apps/studio/
|
|
const touchesDashboard = files.some((file) => file.filename.startsWith(DASHBOARD_PATH))
|
|
|
|
if (touchesDashboard) {
|
|
const hoursOld = Math.floor((Date.now() - createdAt.getTime()) / (1000 * 60 * 60))
|
|
const daysOld = Math.floor(hoursOld / 24)
|
|
|
|
// Fetch review status
|
|
let reviewStatus = 'no-reviews'
|
|
let reviewEmoji = ':eyes:'
|
|
try {
|
|
const { data: reviews } = await github.rest.pulls.listReviews({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: pr.number,
|
|
per_page: 100,
|
|
})
|
|
|
|
if (reviews.length === 0) {
|
|
reviewStatus = 'no-reviews'
|
|
reviewEmoji = ':eyes:'
|
|
} else {
|
|
// Get the most recent review from each reviewer
|
|
const latestReviews = {}
|
|
reviews.forEach((review) => {
|
|
if (
|
|
!latestReviews[review.user.login] ||
|
|
new Date(review.submitted_at) >
|
|
new Date(latestReviews[review.user.login].submitted_at)
|
|
) {
|
|
latestReviews[review.user.login] = review
|
|
}
|
|
})
|
|
|
|
// Check for most critical state (Changes Requested > Approved > Commented)
|
|
const states = Object.values(latestReviews).map((r) => r.state)
|
|
if (states.includes('CHANGES_REQUESTED')) {
|
|
reviewStatus = 'changes-requested'
|
|
reviewEmoji = ':warning:'
|
|
} else if (states.includes('APPROVED')) {
|
|
reviewStatus = 'approved'
|
|
reviewEmoji = ':heavy_check_mark:'
|
|
} else {
|
|
reviewStatus = 'commented'
|
|
reviewEmoji = ':speech_balloon:'
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log(
|
|
`Warning: Could not fetch review status for PR #${pr.number}: ${error.message}`
|
|
)
|
|
}
|
|
|
|
// Get mergeable state
|
|
let mergeableStatus = 'unknown'
|
|
let mergeableEmoji = ':grey_question:'
|
|
|
|
// Fetch full PR details to get mergeable state (it's not always in the list response)
|
|
try {
|
|
const { data: fullPR } = await github.rest.pulls.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: pr.number,
|
|
})
|
|
|
|
const mergeableState = fullPR.mergeable_state
|
|
|
|
switch (mergeableState) {
|
|
case 'clean':
|
|
mergeableStatus = 'ready'
|
|
mergeableEmoji = ':rocket:'
|
|
break
|
|
case 'dirty':
|
|
mergeableStatus = 'conflicts'
|
|
mergeableEmoji = ':collision:'
|
|
break
|
|
case 'blocked':
|
|
mergeableStatus = 'blocked'
|
|
mergeableEmoji = ':no_entry:'
|
|
break
|
|
case 'unstable':
|
|
mergeableStatus = 'unstable'
|
|
mergeableEmoji = ':warning:'
|
|
break
|
|
case 'behind':
|
|
mergeableStatus = 'behind'
|
|
mergeableEmoji = ':arrow_down:'
|
|
break
|
|
case 'draft':
|
|
mergeableStatus = 'draft'
|
|
mergeableEmoji = ':pencil2:'
|
|
break
|
|
default:
|
|
mergeableStatus = mergeableState || 'unknown'
|
|
mergeableEmoji = ':grey_question:'
|
|
}
|
|
} catch (error) {
|
|
console.log(
|
|
`Warning: Could not fetch mergeable state for PR #${pr.number}: ${error.message}`
|
|
)
|
|
}
|
|
|
|
// Skip PRs that have already been actioned (reviewed)
|
|
if (reviewStatus !== 'no-reviews') {
|
|
console.log(`PR #${pr.number} has already been reviewed (${reviewStatus}), skipping...`)
|
|
continue
|
|
}
|
|
|
|
// Skip PRs with merge conflicts
|
|
if (mergeableStatus === 'conflicts') {
|
|
console.log(`PR #${pr.number} has merge conflicts, skipping...`)
|
|
continue
|
|
}
|
|
|
|
stalePRs.push({
|
|
number: pr.number,
|
|
title: pr.title,
|
|
url: pr.html_url,
|
|
author: pr.user.login,
|
|
createdAt: pr.created_at,
|
|
hoursOld: hoursOld,
|
|
daysOld: daysOld,
|
|
fileCount: files.filter((f) => f.filename.startsWith(DASHBOARD_PATH)).length,
|
|
reviewStatus: reviewStatus,
|
|
reviewEmoji: reviewEmoji,
|
|
mergeableStatus: mergeableStatus,
|
|
mergeableEmoji: mergeableEmoji,
|
|
})
|
|
|
|
console.log(
|
|
`✓ Found stale Dashboard PR #${pr.number} (Review: ${reviewStatus}, Mergeable: ${mergeableStatus})`
|
|
)
|
|
}
|
|
}
|
|
|
|
page++
|
|
}
|
|
|
|
console.log(`Found ${stalePRs.length} stale Dashboard PRs`)
|
|
|
|
// Sort by age (newest first)
|
|
stalePRs.sort((a, b) => a.hoursOld - b.hoursOld)
|
|
|
|
// Store results for next step
|
|
core.setOutput('stale_prs', JSON.stringify(stalePRs))
|
|
core.setOutput('count', stalePRs.length)
|
|
|
|
return stalePRs
|
|
}
|