@@ -2,9 +2,9 @@ name: Slack Open PR Nag Bot
22
33on :
44 schedule :
5- # Runs at 7am ET
6- - cron : ' 0 7 * * 1-5 '
7- timezone : ' America/New_York '
5+ # NOTE: GitHub Actions only supports UTC. The 'timezone' property is invalid here.
6+ # 11:00 UTC is 7:00 AM EST / 8:00 AM EDT.
7+ - cron : ' 0 11 * * 1-5 '
88 workflow_dispatch : # Allows manual testing
99
1010permissions :
2626 MANUAL_RUN : ${{ github.event_name == 'workflow_dispatch' }}
2727
2828 # CENTRAL USER REGISTRY
29- # Maps GitHub usernames to both their Slack Member ID and Document Display Name
3029 USER_MAPPING : |
3130 {
3231 "aaronb-stacks": { "id": "U067F1SRYT0", "name": "Aaron" },
3837 "federico-stacks": { "id": "U08ASJ1LKEV", "name": "Federico" },
3938 "francesco-stacks": { "id": "U07MCETKTKN", "name": "Francesco" },
4039 "hstove-stacks": { "id": "U068W8VBSQM", "name": "Hank" },
41- "radu-stacks": { "id": "U0ANRRXD080 ", "name": "Radu" },
40+ "radu-stacks": { "id": "U09NA1TP8PK ", "name": "Radu" },
4241 "rob-stacks": { "id": "U07UNFAS4BD", "name": "Roberto" },
4342 "simone-stacks": { "id": "U09NA1TP8PK", "name": "Simone" }
4443 }
@@ -61,27 +60,30 @@ jobs:
6160 let userMap = {};
6261 try {
6362 const rawMap = JSON.parse(process.env.USER_MAPPING || '{}');
64- // Ensure keys are lowercase for foolproof matching
6563 userMap = Object.fromEntries(Object.entries(rawMap).map(([k, v]) => [k.toLowerCase(), v]));
6664 } catch (e) {
67- core.setFailed('Error parsing USER_MAPPING JSON. Check your JSON syntax. ');
65+ core.setFailed('Error parsing USER_MAPPING JSON.');
6866 return;
6967 }
7068
71- // Whitelist is derived from the mapping keys
7269 const allowedUsers = new Set(Object.keys(userMap));
7370
74- // Helper to get doc display name from the mapping block
7571 const getDocName = (username) => {
7672 const clean = username.toLowerCase();
77- if (userMap[clean] && userMap[clean].name) {
78- return userMap[clean].name;
79- }
80- // Intelligent fallback: strip suffix and capitalize
73+ if (userMap[clean] && userMap[clean].name) return userMap[clean].name;
8174 const base = clean.replace('-stacks', '');
8275 return base.charAt(0).toUpperCase() + base.slice(1);
8376 };
8477
78+ // Helper to clean titles so they do not break Slack's control sequences
79+ const sanitizeTitle = (title) => {
80+ return title
81+ .replace(/&/g, '&')
82+ .replace(/</g, '<')
83+ .replace(/>/g, '>')
84+ .replace(/\|/g, '—'); // Swap out pipes to protect link parsing
85+ };
86+
8587 // 2. Fetch open Pull Requests
8688 const { data: prs } = await github.rest.pulls.list({
8789 owner,
@@ -102,28 +104,44 @@ jobs:
102104 return;
103105 }
104106
105- // 3. Build the plain text list strings
106- const lines = [];
107- lines.push(`*📋 Open PRs*${manualRun ? ' _(Manual Run)_' : ''}\n`);
107+ // 3. Construct the payload blocks incrementally to avoid limits
108+ const payload = { blocks: [] };
109+
110+ // Add initial title header block
111+ payload.blocks.push({
112+ type: 'section',
113+ text: {
114+ type: 'mrkdwn',
115+ text: `*📋 Open PRs*${manualRun ? ' _(Manual Run)_' : ''}\n`
116+ }
117+ });
108118
119+ // Distribute lines across multiple blocks dynamically
120+ let currentLines = [];
109121 for (const pr of filteredSortedPrs) {
110122 const author = getDocName(pr.user.login);
123+ const title = sanitizeTitle(pr.title);
111124 const reviewers = pr.requested_reviewers.map(r => getDocName(r.login)).join(', ') || 'Anyone';
112125
113- lines.push(`${author} <${pr.html_url}|${pr.title}> (${reviewers})`);
114- }
126+ currentLines.push(`${author} <${pr.html_url}|${title}> (${reviewers})`);
115127
116- const payload = {
117- blocks: [
118- {
128+ // Create a new layout block every 8 PRs to comfortably avoid the 3000 char ceiling
129+ if (currentLines.length === 8) {
130+ payload.blocks.push( {
119131 type: 'section',
120- text: {
121- type: 'mrkdwn',
122- text: lines.join('\n')
123- }
124- }
125- ]
126- };
132+ text: { type: 'mrkdwn', text: currentLines.join('\n') }
133+ });
134+ currentLines = [];
135+ }
136+ }
137+
138+ // Append any remaining PR lines
139+ if (currentLines.length > 0) {
140+ payload.blocks.push({
141+ type: 'section',
142+ text: { type: 'mrkdwn', text: currentLines.join('\n') }
143+ });
144+ }
127145
128146 // 4. Send payload via Webhook
129147 const response = await fetch(webhookUrl, {
0 commit comments