Add Discord API rate limit handling and bump @pulumi/github (#84)

- Handle 429 responses from Discord API with automatic retry based on
  retry_after, preventing transient failures during role sync
- Surface underlying error details in role and member role read failures
  to make debugging easier
- Bump @pulumi/github to ^6.11.0 (resolves 6.12.1)

🏠 Remote-Dev: homespace
This commit is contained in:
Den Delimarsky 2026-03-20 01:08:13 -07:00 • committed by GitHub
commit 3a06068d55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 24 deletions

8
package-lock.json generated
View file

@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@pulumi/github": "^6.7.3",
"@pulumi/github": "^6.11.0",
"@pulumi/googleworkspace": "file:sdks/googleworkspace",
"@pulumi/pulumi": "^3.218.0",
"@pulumi/random": "^4.14.0"
@ -655,9 +655,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@pulumi/github": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@pulumi/github/-/github-6.9.1.tgz",
"integrity": "sha512-B7FcceruXfMClws4poIyUfJoy2dqcdhi8P5+ahfArIeaDjFDlcRNU9SZbfFRbkdoXflMJi+AI3rWabyvqS2+YQ==",
"version": "6.12.1",
"resolved": "https://registry.npmjs.org/@pulumi/github/-/github-6.12.1.tgz",
"integrity": "sha512-OsbQgF8go/tA1PuMhFq7O0XVfYbwRk0/LHaZTH6OFO395SufdFFa1go3IeXvJCSA5oDCbdULerGg4n9sP8vTLg==",
"license": "Apache-2.0",
"dependencies": {
"@pulumi/pulumi": "^3.142.0"

View file

@ -20,31 +20,63 @@ interface DiscordApiError {
message: string;
}
interface DiscordRateLimitResponse {
message: string;
retry_after: number;
global: boolean;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function discordFetch<T>(
token: string,
endpoint: string,
options: RequestInit = {}
options: RequestInit = {},
maxRetries = 5
): Promise<T> {
const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, {
...options,
headers: {
Authorization: `Bot ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
let lastError: Error | undefined;
if (!response.ok) {
const error = (await response.json()) as DiscordApiError;
throw new Error(`Discord API error: ${error.message} (code: ${error.code})`);
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, {
...options,
headers: {
Authorization: `Bot ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (response.status === 429) {
const body = (await response.json()) as DiscordRateLimitResponse;
const retryAfterMs = Math.ceil(body.retry_after * 1000) + Math.random() * 250;
lastError = new Error(
`Discord API rate limited on ${endpoint} (retry_after=${body.retry_after}s, global=${body.global})`
);
if (attempt < maxRetries) {
await sleep(retryAfterMs);
continue;
}
throw lastError;
}
if (!response.ok) {
const error = (await response.json()) as DiscordApiError;
throw new Error(`Discord API error: ${error.message} (code: ${error.code})`);
}
// Handle 204 No Content
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
}
// Handle 204 No Content
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
throw (
lastError ?? new Error(`Discord API request to ${endpoint} failed after ${maxRetries} retries`)
);
}
// Discord API response types
@ -122,8 +154,8 @@ const discordRoleProvider: pulumi.dynamic.ResourceProvider = {
roleId: role.id,
},
};
} catch {
throw new Error(`Failed to read role ${id}`);
} catch (error) {
throw new Error(`Failed to read role ${id}: ${error}`);
}
},
@ -303,7 +335,7 @@ const discordMemberRoleSyncProvider: pulumi.dynamic.ResourceProvider = {
},
};
}
throw new Error(`Failed to read member roles for ${id}`);
throw new Error(`Failed to read member roles for ${id}: ${error}`);
}
const currentRoles = new Set(member.roles);