From c13cb81e76d8f4f1fccdc99ed0708054479097f5 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:40:36 +0000 Subject: [PATCH] chore: remove noisy dashboard PR-reminder workflow (#48142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _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 Co-authored-by: Ali Waseem --- .github/workflows/dashboard-pr-reminder.yml | 45 --- scripts/actions/find-stale-dashboard-prs.ts | 272 ------------------ scripts/actions/send-slack-pr-notification.ts | 140 --------- 3 files changed, 457 deletions(-) delete mode 100644 .github/workflows/dashboard-pr-reminder.yml delete mode 100644 scripts/actions/find-stale-dashboard-prs.ts delete mode 100644 scripts/actions/send-slack-pr-notification.ts diff --git a/.github/workflows/dashboard-pr-reminder.yml b/.github/workflows/dashboard-pr-reminder.yml deleted file mode 100644 index 9b4470882f2..00000000000 --- a/.github/workflows/dashboard-pr-reminder.yml +++ /dev/null @@ -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 diff --git a/scripts/actions/find-stale-dashboard-prs.ts b/scripts/actions/find-stale-dashboard-prs.ts deleted file mode 100644 index d6e8df7d54e..00000000000 --- a/scripts/actions/find-stale-dashboard-prs.ts +++ /dev/null @@ -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 = { - 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 { - 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 = {} - 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) - }) diff --git a/scripts/actions/send-slack-pr-notification.ts b/scripts/actions/send-slack-pr-notification.ts deleted file mode 100644 index 7396c8a7af7..00000000000 --- a/scripts/actions/send-slack-pr-notification.ts +++ /dev/null @@ -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, '>') -} - -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 { - 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) - })