7575 github-token : ${{ github.token }}
7676 script : |
7777 const githubLogin = context.payload.sender.login.toLowerCase();
78+ const lookupStartedAt = Date.now();
7879
7980 core.info(`Looking up Slack user for @${githubLogin}...`);
8081 core.setOutput('slack_user_id', '');
@@ -85,59 +86,127 @@ jobs:
8586 throw new Error('SLACK_DOCS_BOT_TOKEN is not configured.');
8687 }
8788
89+ const sleep = (milliseconds) =>
90+ new Promise((resolve) => setTimeout(resolve, milliseconds));
91+ const maxRetries = 5;
92+ const minimumRequestIntervalMs = 3000;
93+ let lastRequestAt = 0;
94+
95+ async function fetchUsersPage(url, pageNumber) {
96+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
97+ const spacingDelay = Math.max(
98+ 0,
99+ minimumRequestIntervalMs - (Date.now() - lastRequestAt),
100+ );
101+ if (spacingDelay > 0) {
102+ await sleep(spacingDelay);
103+ }
104+
105+ lastRequestAt = Date.now();
106+ const requestStartedAt = lastRequestAt;
107+ const response = await fetch(url, {
108+ headers: {
109+ Authorization: `Bearer ${token}`,
110+ },
111+ });
112+ const requestDurationMs = Date.now() - requestStartedAt;
113+
114+ if (response.status === 429) {
115+ if (attempt === maxRetries) {
116+ throw new Error(
117+ `Slack users.list page ${pageNumber} remained rate-limited after ${maxRetries} retries.`,
118+ );
119+ }
120+
121+ const retryAfterSeconds = Number(response.headers.get('retry-after'));
122+ const retryDelayMs =
123+ Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
124+ ? retryAfterSeconds * 1000
125+ : Math.min(1000 * 2 ** attempt, 30000);
126+
127+ core.warning(
128+ `Slack users.list page ${pageNumber} was rate-limited (attempt ${attempt + 1}/${maxRetries + 1}); retrying in ${retryDelayMs} ms.`,
129+ );
130+ await sleep(retryDelayMs);
131+ continue;
132+ }
133+
134+ if (!response.ok) {
135+ throw new Error(
136+ `Slack users.list page ${pageNumber} returned HTTP ${response.status} after ${requestDurationMs} ms.`,
137+ );
138+ }
139+
140+ if (attempt > 0) {
141+ core.info(
142+ `Slack users.list page ${pageNumber} succeeded after ${attempt} retry attempt(s) in ${requestDurationMs} ms.`,
143+ );
144+ } else {
145+ core.info(
146+ `Slack users.list page ${pageNumber} returned HTTP ${response.status} in ${requestDurationMs} ms.`,
147+ );
148+ }
149+
150+ return response.json();
151+ }
152+
153+ throw new Error(`Slack users.list page ${pageNumber} request did not complete.`);
154+ }
155+
88156 let cursor = '';
89157 let pages = 0;
90- const matches = [];
91158
92159 do {
93- core.info(`Fetching Slack users.list page ${pages + 1}...`);
160+ const pageNumber = pages + 1;
161+ core.info(`Fetching Slack users.list page ${pageNumber}...`);
94162 const url = new URL('https://slack.com/api/users.list');
95163 url.searchParams.set('limit', '200');
96164 if (cursor) {
97165 url.searchParams.set('cursor', cursor);
98166 }
99167
100- const response = await fetch(url, {
101- headers: {
102- Authorization: `Bearer ${token}`,
103- },
104- });
105-
106- if (!response.ok) {
107- throw new Error(`Slack users.list returned HTTP ${response.status}.`);
108- }
109-
110- const result = await response.json();
168+ const result = await fetchUsersPage(url, pageNumber);
111169 if (!result.ok) {
112- throw new Error(`Slack users.list failed: ${result.error || 'unknown error'}.`);
170+ throw new Error(
171+ `Slack users.list page ${pageNumber} failed: ${result.error || 'unknown error'}.`,
172+ );
113173 }
114174
115175 if (!Array.isArray(result.members)) {
116- throw new Error('Slack users.list response did not include a members array.');
176+ throw new Error(
177+ `Slack users.list page ${pageNumber} did not include a members array.`,
178+ );
117179 }
118180
119181 pages += 1;
120- matches.push(...result.members.filter((member) =>
182+ core.info(`Slack users.list page ${pages} contained ${result.members.length} member records.`);
183+ const match = result.members.find((member) =>
121184 !member.deleted &&
122185 !member.is_bot &&
123186 !member.is_app_user &&
124187 typeof member.id === 'string' &&
125188 typeof member.name === 'string' &&
126189 member.name.toLowerCase() === githubLogin
127- ));
190+ );
191+
192+ if (match) {
193+ core.info(
194+ `Found the matching Slack username on page ${pages} after ${Date.now() - lookupStartedAt} ms.`,
195+ );
196+ core.setOutput('slack_user_id', match.id);
197+ core.setOutput('slack_mention', `<@${match.id}>`);
198+ return;
199+ }
128200
129201 cursor = result.response_metadata?.next_cursor || '';
202+ core.info(
203+ `No matching Slack username on page ${pages}; ${cursor ? 'continuing pagination.' : 'no more pages.'}`,
204+ );
130205 } while (cursor);
131206
132- core.info(`Checked ${pages} pages of users.list. Found ${matches.length} match(es) for @${githubLogin}.`);
133- if (matches.length !== 1) {
134- core.warning(`Expected one Slack username matching @${githubLogin}; found ${matches.length}. No DM will be sent.`);
135- return;
136- }
137-
138- core.info(`Will DM Slack user ${matches[0].id}.`);
139- core.setOutput('slack_user_id', matches[0].id);
140- core.setOutput('slack_mention', `<@${matches[0].id}>`);
207+ core.warning(
208+ `No active Slack username matched @${githubLogin} after ${pages} page(s) and ${Date.now() - lookupStartedAt} ms; no DM will be sent.`,
209+ );
141210
142211 - name : DM the author in Slack
143212 id : slack_dm
0 commit comments