@@ -16,114 +16,16 @@ jobs:
1616 check-dashboard-prs :
1717 runs-on : ubuntu-latest
1818 steps :
19+ - name : Checkout repository
20+ uses : actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
21+
1922 - name : Find Dashboard PRs older than 24 hours
2023 id : find-prs
2124 uses : actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
2225 with :
2326 script : |
24- const TWENTY_FOUR_HOURS_AGO = new Date(Date.now() - 24 * 60 * 60 * 1000);
25- const DASHBOARD_PATH = 'apps/studio/';
26-
27- console.log(`Looking for PRs older than: ${TWENTY_FOUR_HOURS_AGO.toISOString()}`);
28-
29- const stalePRs = [];
30- let page = 1;
31- let hasMore = true;
32-
33- // Fetch PRs page by page, newest first
34- while (hasMore && page <= 10) { // Limit to 10 pages (1000 PRs) as safety measure
35- console.log(`Fetching page ${page}...`);
36-
37- const { data: prs } = await github.rest.pulls.list({
38- owner: context.repo.owner,
39- repo: context.repo.repo,
40- state: 'open',
41- sort: 'created',
42- direction: 'desc',
43- per_page: 100,
44- page: page
45- });
46-
47- if (prs.length === 0) {
48- hasMore = false;
49- break;
50- }
51-
52- // Check each PR
53- for (const pr of prs) {
54- // Skip PRs from forks - only check internal PRs
55- if (pr.head.repo && pr.head.repo.full_name !== context.repo.owner + '/' + context.repo.repo) {
56- console.log(`PR #${pr.number} is from a fork, skipping...`);
57- continue;
58- }
59-
60- // Skip dependabot PRs
61- if (pr.user.login === 'dependabot[bot]' || pr.user.login === 'dependabot') {
62- console.log(`PR #${pr.number} is from dependabot, skipping...`);
63- continue;
64- }
65-
66- // Skip draft PRs
67- if (pr.draft) {
68- console.log(`PR #${pr.number} is a draft, skipping...`);
69- continue;
70- }
71-
72- const createdAt = new Date(pr.created_at);
73-
74- // If this PR is newer than 24 hours, skip it
75- if (createdAt > TWENTY_FOUR_HOURS_AGO) {
76- console.log(`PR #${pr.number} is too new, skipping...`);
77- continue;
78- }
79-
80- // If we've reached PRs older than what we're checking, we can stop
81- // But we still need to check if they touch Dashboard files
82- console.log(`Checking PR #${pr.number}: ${pr.title}`);
83-
84- // Fetch files changed in this PR
85- const { data: files } = await github.rest.pulls.listFiles({
86- owner: context.repo.owner,
87- repo: context.repo.repo,
88- pull_number: pr.number,
89- per_page: 100
90- });
91-
92- // Check if any file is under apps/studio/
93- const touchesDashboard = files.some(file => file.filename.startsWith(DASHBOARD_PATH));
94-
95- if (touchesDashboard) {
96- const hoursOld = Math.floor((Date.now() - createdAt.getTime()) / (1000 * 60 * 60));
97- const daysOld = Math.floor(hoursOld / 24);
98-
99- stalePRs.push({
100- number: pr.number,
101- title: pr.title,
102- url: pr.html_url,
103- author: pr.user.login,
104- createdAt: pr.created_at,
105- hoursOld: hoursOld,
106- daysOld: daysOld,
107- fileCount: files.filter(f => f.filename.startsWith(DASHBOARD_PATH)).length
108- });
109-
110- console.log(`✓ Found stale Dashboard PR #${pr.number}`);
111- }
112- }
113-
114- page++;
115- }
116-
117- console.log(`Found ${stalePRs.length} stale Dashboard PRs`);
118-
119- // Sort by age (oldest first)
120- stalePRs.sort((a, b) => a.hoursOld - b.hoursOld);
121-
122- // Store results for next step
123- core.setOutput('stale_prs', JSON.stringify(stalePRs));
124- core.setOutput('count', stalePRs.length);
125-
126- return stalePRs;
27+ const findStalePRs = require('./scripts/actions/find-stale-dashboard-prs.js');
28+ return await findStalePRs({ github, context, core });
12729
12830 - name : Send Slack notification
12931 if : fromJSON(steps.find-prs.outputs.count) > 0
@@ -133,87 +35,10 @@ jobs:
13335 STALE_PRS_JSON : ${{ steps.find-prs.outputs.stale_prs }}
13436 with :
13537 script : |
38+ const sendSlackNotification = require('./scripts/actions/send-slack-pr-notification.js');
13639 const stalePRs = JSON.parse(process.env.STALE_PRS_JSON);
137- const count = stalePRs.length;
138-
139- // Build PR blocks with proper escaping for Slack mrkdwn
140- const prBlocks = stalePRs.map(pr => {
141- // Format age display
142- const remainingHours = pr.hoursOld % 24;
143- const ageText = pr.daysOld > 0
144- ? `${pr.daysOld}d ${remainingHours}h`
145- : `${pr.hoursOld}h`;
146-
147- // Escape special characters for Slack mrkdwn (escape &, <, >)
148- const escapeSlack = (text) => {
149- return text
150- .replace(/&/g, '&')
151- .replace(/</g, '<')
152- .replace(/>/g, '>');
153- };
154-
155- // Truncate title if too long (max 3000 chars for entire text field)
156- const maxTitleLength = 200;
157- const safeTitle = pr.title.length > maxTitleLength
158- ? escapeSlack(pr.title.substring(0, maxTitleLength) + '...')
159- : escapeSlack(pr.title);
160-
161- return {
162- type: "section",
163- text: {
164- type: "mrkdwn",
165- text: `*<${pr.url}|#${pr.number}: ${safeTitle}>*\n:bust_in_silhouette: @${pr.author} • :clock3: ${ageText} old • :file_folder: ${pr.fileCount} Dashboard files`
166- }
167- };
168- });
169-
170- // Slack has a 50 block limit, we use 3 for header/intro/divider
171- // So we can show max 47 PRs
172- const MAX_PRS_TO_SHOW = 47;
173- const prBlocksToShow = prBlocks.slice(0, MAX_PRS_TO_SHOW);
174- const hasMorePRs = prBlocks.length > MAX_PRS_TO_SHOW;
175-
176- // Build complete Slack message
177- const slackMessage = {
178- text: "Dashboard PRs needing attention",
179- blocks: [
180- {
181- type: "header",
182- text: {
183- type: "plain_text",
184- text: "Dashboard PRs Older Than 24 Hours"
185- }
186- },
187- {
188- type: "section",
189- text: {
190- type: "mrkdwn",
191- text: `There are *${count}* open PRs affecting /apps/studio/ that are older than 24 hours:${hasMorePRs ? ` (showing first ${MAX_PRS_TO_SHOW})` : ''}`
192- }
193- },
194- {
195- type: "divider"
196- },
197- ...prBlocksToShow
198- ]
199- };
200-
201- // Send to Slack
20240 const webhookUrl = process.env.SLACK_WEBHOOK_URL;
203- const response = await fetch(webhookUrl, {
204- method: 'POST',
205- headers: {
206- 'Content-Type': 'application/json',
207- },
208- body: JSON.stringify(slackMessage)
209- });
210-
211- if (!response.ok) {
212- const errorText = await response.text();
213- throw new Error(`Slack notification failed: ${response.status} ${response.statusText}\n${errorText}`);
214- }
215-
216- console.log('✓ Slack notification sent successfully!');
41+ await sendSlackNotification(stalePRs, webhookUrl);
21742
21843 - name : No stale PRs found
21944 if : fromJSON(steps.find-prs.outputs.count) == 0
0 commit comments