Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion scripts/data/sponsors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { fetchWithRetry } from '../utils/fetch.mjs';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const OUTPUT = join(ROOT, 'generated', 'sponsors.json');
Expand Down Expand Up @@ -78,7 +79,7 @@ const fetchAllOrders = async () => {
const all = [];

while (offset < totalCount) {
const res = await fetch(API, {
const res = await fetchWithRetry(API, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
Expand Down
3 changes: 2 additions & 1 deletion scripts/markdown/governance.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fetchWithRetry } from '../utils/fetch.mjs';

const { GH_TOKEN } = process.env;

Expand Down Expand Up @@ -57,7 +58,7 @@ await mkdir(outputDir, { recursive: true });
const results = await Promise.all(
Object.entries(FILE_MAP).map(async ([source, { output, label }]) => {
const url = `https://raw.githubusercontent.com/webpack/governance/HEAD/${source}`;
const res = await fetch(url, { headers: BASE_HEADERS });
const res = await fetchWithRetry(url, { headers: BASE_HEADERS });

if (!res.ok) {
console.error(`Failed: ${source} -> ${res.status} ${res.statusText}`);
Expand Down
5 changes: 3 additions & 2 deletions scripts/markdown/readmes.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fetchWithRetry } from '../utils/fetch.mjs';

const { GH_TOKEN } = process.env;

Expand All @@ -18,7 +19,7 @@ const discoverRepos = async () => {
'https://api.github.com/orgs/webpack/repos?per_page=100&type=public';

while (url) {
const res = await fetch(url, { headers: BASE_HEADERS });
const res = await fetchWithRetry(url, { headers: BASE_HEADERS });

for (const repo of await res.json()) {
if (repo.archived) continue;
Expand Down Expand Up @@ -50,7 +51,7 @@ const repoName = fullName => fullName.split('/')[1];

const fetchReadme = async fullName => {
const url = `https://raw.githubusercontent.com/${fullName}/HEAD/README.md`;
const res = await fetch(url);
const res = await fetchWithRetry(url);
return res.text();
};

Expand Down
42 changes: 42 additions & 0 deletions scripts/utils/fetch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// fetch() wrapper that retries flakey responses like the GitHub and Open Collective
// occasionally 503 midbuild,so one blip doesn't fail the whole deploy.

const RETRYABLE = new Set([429, 502, 503, 504]);

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

// Use the server's Retry After when it sends one, otherwise back off.
const delayFor = (attempt, baseDelay, response) => {
const retryAfter = Number(response?.headers.get('retry-after'));
if (retryAfter > 0) return retryAfter * 1000;
return baseDelay * 2 ** attempt + Math.random() * baseDelay;
};

/**
* @param {string | URL} url
* @param {RequestInit} [options]
* @param {{ retries?: number, baseDelay?: number, timeout?: number }} [config]
* @returns {Promise<Response>}
*/
export const fetchWithRetry = async (
url,
options,
{ retries = 3, baseDelay = 500, timeout = 15000 } = {}
) => {
for (let attempt = 0; ; attempt++) {
let response;

try {
response = await fetch(url, {
...options,
signal: AbortSignal.timeout(timeout),
});
if (response.ok || !RETRYABLE.has(response.status)) return response;
} catch (error) {
if (attempt === retries) throw error;
}

if (attempt === retries) return response;
await sleep(delayFor(attempt, baseDelay, response));
}
};