mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore: remove noisy dashboard PR-reminder workflow (#48142)
<!-- ccr-slack-attribution --> _Requested by **Ivan Vasilov** · [Slack thread](https://supabase.slack.com/archives/C0161K73J1J/p1784635673434979)_ ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Chore / cleanup — removes a scheduled GitHub Actions workflow. ## What is the current behavior? The `Dashboard PR Reminder` workflow (`.github/workflows/dashboard-pr-reminder.yml`) runs on a schedule and posts a "Dashboard PRs Older Than 24 Hours" reminder to Slack. It has become too noisy — Jordi flagged that it fired 5 times in 3 days. The #team-frontend team agreed to remove it rather than reschedule it. ## What is the new behavior? The workflow and its exclusively-used supporting scripts are deleted, so the Slack reminder no longer runs. Files removed (each used exclusively by this workflow): - `.github/workflows/dashboard-pr-reminder.yml` — the reminder workflow itself. - `scripts/actions/find-stale-dashboard-prs.ts` — helper invoked only by this workflow's run step; not referenced anywhere else in the repo. - `scripts/actions/send-slack-pr-notification.ts` — helper invoked only by this workflow's run step; not referenced anywhere else in the repo. (This leaves `scripts/actions/` empty, so the directory is removed too.) No shared files were touched. The workflow's `sparse-checkout` of `scripts`/`patches`, `.nvmrc`, and `pnpm-lock.yaml` are repo-wide and remain in place. ## Additional context Verified via a full-repo grep that the two scripts and the workflow file are referenced nowhere outside this workflow before deleting them. --- _Generated by [Claude Code](https://claude.ai/code/session_01RynCtzP874KrpN8CPf7n7n)_ Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Ali Waseem <waseema393@gmail.com>
This commit is contained in:
3 files changed
-457
No files matched your search
@@ -1,45 +0,0 @@
|
||||
name: Dashboard PR Reminder
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 10am Singapore Time (2am UTC)
|
||||
- cron: '0 2 * * *'
|
||||
# Run at 10am US Eastern Time (2pm UTC = 10am EDT / 9am EST)
|
||||
- cron: '0 14 * * *'
|
||||
workflow_dispatch: # Allow manual trigger for testing
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-dashboard-prs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
scripts
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Find stale Dashboard PRs and notify Slack
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DASHBOARD_WEBHOOK_URL }}
|
||||
run: pnpm tsx scripts/actions/find-stale-dashboard-prs.ts | pnpm tsx scripts/actions/send-slack-pr-notification.ts
|
||||
@@ -1,272 +0,0 @@
|
||||
const TWENTY_FOUR_HOURS_AGO = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||
const DASHBOARD_PATH = 'apps/studio/'
|
||||
const REPO_OWNER = 'supabase'
|
||||
const REPO_NAME = 'supabase'
|
||||
|
||||
const GITHUB_TOKEN = process.env.GITHUB_TOKEN
|
||||
|
||||
class RateLimitError extends Error {
|
||||
constructor(resetAt: string) {
|
||||
super(`GitHub API rate limit exceeded. Resets at ${resetAt}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function githubApi(path: string) {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
}
|
||||
if (GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${GITHUB_TOKEN}`
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}${path}`, {
|
||||
headers,
|
||||
})
|
||||
|
||||
if (
|
||||
response.status === 429 ||
|
||||
(response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0')
|
||||
) {
|
||||
const resetEpoch = response.headers.get('x-ratelimit-reset')
|
||||
const resetAt = resetEpoch ? new Date(Number(resetEpoch) * 1000).toISOString() : 'unknown'
|
||||
throw new RateLimitError(resetAt)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}\n${errorText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
interface StalePR {
|
||||
number: number
|
||||
title: string
|
||||
url: string
|
||||
author: string
|
||||
createdAt: string
|
||||
hoursOld: number
|
||||
daysOld: number
|
||||
fileCount: number
|
||||
reviewStatus: string
|
||||
reviewEmoji: string
|
||||
mergeableStatus: string
|
||||
mergeableEmoji: string
|
||||
}
|
||||
|
||||
async function findStalePRs(): Promise<StalePR[]> {
|
||||
console.error(`Looking for PRs older than: ${TWENTY_FOUR_HOURS_AGO.toISOString()}`)
|
||||
|
||||
const stalePRs: StalePR[] = []
|
||||
let page = 1
|
||||
let hasMore = true
|
||||
|
||||
outer: while (hasMore && page <= 10) {
|
||||
console.error(`Fetching page ${page}...`)
|
||||
|
||||
let prs: any[]
|
||||
try {
|
||||
prs = await githubApi(
|
||||
`/pulls?state=open&sort=created&direction=desc&per_page=100&page=${page}`
|
||||
)
|
||||
} catch (error: any) {
|
||||
if (error instanceof RateLimitError) {
|
||||
console.error(`Rate limited while listing PRs. ${error.message}`)
|
||||
break
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
if (prs.length === 0) {
|
||||
hasMore = false
|
||||
break
|
||||
}
|
||||
|
||||
for (const pr of prs) {
|
||||
// Skip PRs from forks
|
||||
if (pr.head.repo && pr.head.repo.full_name !== `${REPO_OWNER}/${REPO_NAME}`) {
|
||||
console.error(`PR #${pr.number} is from a fork, skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip dependabot PRs
|
||||
if (pr.user.login === 'dependabot[bot]' || pr.user.login === 'dependabot') {
|
||||
console.error(`PR #${pr.number} is from dependabot, skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip draft PRs
|
||||
if (pr.draft) {
|
||||
console.error(`PR #${pr.number} is a draft, skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip closed PRs
|
||||
if (pr.state === 'closed') {
|
||||
console.error(`PR #${pr.number} is closed, skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
const createdAt = new Date(pr.created_at)
|
||||
|
||||
if (createdAt > TWENTY_FOUR_HOURS_AGO) {
|
||||
console.error(`PR #${pr.number} is too new, skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
console.error(`Checking PR #${pr.number}: ${pr.title}`)
|
||||
|
||||
let files: any[]
|
||||
try {
|
||||
files = await githubApi(`/pulls/${pr.number}/files?per_page=100`)
|
||||
} catch (error: any) {
|
||||
if (error instanceof RateLimitError) {
|
||||
console.error(`Rate limited while fetching files. ${error.message}`)
|
||||
break outer
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
const touchesDashboard = files.some((file: any) => file.filename.startsWith(DASHBOARD_PATH))
|
||||
|
||||
if (!touchesDashboard) continue
|
||||
|
||||
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 reviews = await githubApi(`/pulls/${pr.number}/reviews?per_page=100`)
|
||||
|
||||
if (reviews.length > 0) {
|
||||
const latestReviews: Record<string, any> = {}
|
||||
reviews.forEach((review: any) => {
|
||||
if (
|
||||
!latestReviews[review.user.login] ||
|
||||
new Date(review.submitted_at) >
|
||||
new Date(latestReviews[review.user.login].submitted_at)
|
||||
) {
|
||||
latestReviews[review.user.login] = review
|
||||
}
|
||||
})
|
||||
|
||||
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:'
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error instanceof RateLimitError) {
|
||||
console.error(`Rate limited while fetching reviews. ${error.message}`)
|
||||
break outer
|
||||
}
|
||||
console.error(
|
||||
`Warning: Could not fetch review status for PR #${pr.number}: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
// Get mergeable state
|
||||
let mergeableStatus = 'unknown'
|
||||
let mergeableEmoji = ':grey_question:'
|
||||
try {
|
||||
const fullPR = await githubApi(`/pulls/${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: any) {
|
||||
if (error instanceof RateLimitError) {
|
||||
console.error(`Rate limited while fetching mergeable state. ${error.message}`)
|
||||
break outer
|
||||
}
|
||||
console.error(
|
||||
`Warning: Could not fetch mergeable state for PR #${pr.number}: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
// Skip PRs that have already been reviewed
|
||||
if (reviewStatus !== 'no-reviews') {
|
||||
console.error(`PR #${pr.number} has already been reviewed (${reviewStatus}), skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip PRs with merge conflicts
|
||||
if (mergeableStatus === 'conflicts') {
|
||||
console.error(`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,
|
||||
daysOld,
|
||||
fileCount: files.filter((f: any) => f.filename.startsWith(DASHBOARD_PATH)).length,
|
||||
reviewStatus,
|
||||
reviewEmoji,
|
||||
mergeableStatus,
|
||||
mergeableEmoji,
|
||||
})
|
||||
|
||||
console.error(
|
||||
`Found stale Dashboard PR #${pr.number} (Review: ${reviewStatus}, Mergeable: ${mergeableStatus})`
|
||||
)
|
||||
}
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
console.error(`Found ${stalePRs.length} stale Dashboard PRs`)
|
||||
|
||||
stalePRs.sort((a, b) => a.hoursOld - b.hoursOld)
|
||||
|
||||
return stalePRs
|
||||
}
|
||||
|
||||
findStalePRs()
|
||||
.then((stalePRs) => {
|
||||
// Output JSON to stdout for piping to the next script
|
||||
console.log(JSON.stringify(stalePRs))
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,140 +0,0 @@
|
||||
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL
|
||||
|
||||
if (!SLACK_WEBHOOK_URL) {
|
||||
console.error('SLACK_WEBHOOK_URL environment variable is required')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
interface StalePR {
|
||||
number: number
|
||||
title: string
|
||||
url: string
|
||||
author: string
|
||||
createdAt: string
|
||||
hoursOld: number
|
||||
daysOld: number
|
||||
fileCount: number
|
||||
reviewStatus: string
|
||||
reviewEmoji: string
|
||||
mergeableStatus: string
|
||||
mergeableEmoji: string
|
||||
}
|
||||
|
||||
function escapeSlack(text: string) {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
async function sendSlackNotification(stalePRs: StalePR[]) {
|
||||
const count = stalePRs.length
|
||||
|
||||
const prBlocks = stalePRs.map((pr) => {
|
||||
const remainingHours = pr.hoursOld % 24
|
||||
const ageText = pr.daysOld > 0 ? `${pr.daysOld}d ${remainingHours}h` : `${pr.hoursOld}h`
|
||||
|
||||
const maxTitleLength = 200
|
||||
const safeTitle =
|
||||
pr.title.length > maxTitleLength
|
||||
? escapeSlack(pr.title.substring(0, maxTitleLength) + '...')
|
||||
: escapeSlack(pr.title)
|
||||
|
||||
const reviewStatusText =
|
||||
pr.reviewStatus === 'approved'
|
||||
? 'Approved'
|
||||
: pr.reviewStatus === 'changes-requested'
|
||||
? 'Changes Requested'
|
||||
: pr.reviewStatus === 'commented'
|
||||
? 'Commented'
|
||||
: 'Needs Review'
|
||||
|
||||
const mergeableStatusText =
|
||||
pr.mergeableStatus === 'ready'
|
||||
? 'Ready to Merge'
|
||||
: pr.mergeableStatus === 'conflicts'
|
||||
? 'Has Conflicts'
|
||||
: pr.mergeableStatus === 'blocked'
|
||||
? 'Blocked'
|
||||
: pr.mergeableStatus === 'unstable'
|
||||
? 'Unstable'
|
||||
: pr.mergeableStatus === 'behind'
|
||||
? 'Behind Base'
|
||||
: pr.mergeableStatus === 'draft'
|
||||
? 'Draft'
|
||||
: 'Unknown'
|
||||
|
||||
return {
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `*<${pr.url}|#${pr.number}: ${safeTitle}>*\n:bust_in_silhouette: @${pr.author} • :clock3: ${ageText} old • :file_folder: ${pr.fileCount} Dashboard files\n${pr.reviewEmoji} ${reviewStatusText} • ${pr.mergeableEmoji} ${mergeableStatusText}`,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const MAX_PRS_TO_SHOW = 47
|
||||
const prBlocksToShow = prBlocks.slice(0, MAX_PRS_TO_SHOW)
|
||||
const hasMorePRs = prBlocks.length > MAX_PRS_TO_SHOW
|
||||
|
||||
const slackMessage = {
|
||||
text: 'Dashboard PRs needing attention',
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Dashboard PRs Older Than 24 Hours',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `There are *${count}* open PRs affecting /apps/studio/ that are older than 24 hours:${hasMorePRs ? ` (showing first ${MAX_PRS_TO_SHOW})` : ''}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
...prBlocksToShow,
|
||||
],
|
||||
}
|
||||
|
||||
const response = await fetch(SLACK_WEBHOOK_URL!, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(slackMessage),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(
|
||||
`Slack notification failed: ${response.status} ${response.statusText}\n${errorText}`
|
||||
)
|
||||
}
|
||||
|
||||
console.error('Slack notification sent successfully!')
|
||||
}
|
||||
|
||||
// Read JSON from stdin
|
||||
async function readStdin(): Promise<string> {
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
return Buffer.concat(chunks).toString('utf-8')
|
||||
}
|
||||
|
||||
readStdin()
|
||||
.then(async (input) => {
|
||||
const stalePRs: StalePR[] = JSON.parse(input)
|
||||
|
||||
if (stalePRs.length === 0) {
|
||||
console.error('No stale PRs to notify about')
|
||||
return
|
||||
}
|
||||
|
||||
await sendSlackNotification(stalePRs)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in new issue
Block a user