scripts/cf-rate-limit-retry.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
// Run a Cloudflare CLI command (wrangler) and retry it with backoff only when
// the Cloudflare API rate-limits it ([code: 10429]). Every other failure exits
// immediately with the command's own code.
//
// Why: deploy-worker.yml run 41960944579 (2026-11-05) failed its first API
// call with 10429, so #9881 never reached production and the post-merge deploy
// monitor stayed red until someone re-ran the deploy by hand.
//
// Usage: node scripts/cf-rate-limit-retry.mjs <command> [args...]
// CF_RATE_LIMIT_RETRY_DELAYS_MS overrides the comma-separated backoff
// (default 15000,30000,60000 → up to 4 attempts).
import { spawn } from 'node:child_process';
const RATE_LIMIT = /\[code: 10429\]/;
const DEFAULT_DELAYS_MS = [15_000, 30_000, 60_000];
function parseDelays(raw) {
if (raw === undefined || raw === '') return DEFAULT_DELAYS_MS;
const delays = raw.split(',').map((part) => Number(part.trim()));
if (delays.some((ms) => !Number.isInteger(ms) || ms < 0)) {
throw new Error(`CF_RATE_LIMIT_RETRY_DELAYS_MS must be comma-separated non-negative integers, got "${raw}"`);
}
return delays;
}
function runOnce(command, args) {
return new Promise((resolve) => {
const child = spawn(command, args, { stdio: ['inherit', 'pipe', 'pipe'] });
let output = '';
child.stdout.on('data', (chunk) => {
process.stdout.write(chunk);
output += chunk;
});
child.stderr.on('data', (chunk) => {
process.stderr.write(chunk);
output += chunk;
});
child.on('error', (err) => {
process.stderr.write(`cf-rate-limit-retry: failed to start ${command}: ${err.message}\n`);
resolve({ code: 127, output });
});
child.on('close', (code, signal) => resolve({ code: code ?? (signal ? 1 : 0), output }));
});
}
const [command, ...args] = process.argv.slice(2);
if (!command) {
process.stderr.write('usage: node scripts/cf-rate-limit-retry.mjs <command> [args...]\n');
process.exit(2);
}
let delays;
try {
delays = parseDelays(process.env.CF_RATE_LIMIT_RETRY_DELAYS_MS);
} catch (err) {
process.stderr.write(`cf-rate-limit-retry: ${err.message}\n`);
process.exit(2);
}
const maxAttempts = delays.length + 1;
for (let attempt = 1; ; attempt++) {
const { code, output } = await runOnce(command, args);
if (code === 0) process.exit(0);
if (!RATE_LIMIT.test(output)) process.exit(code);
if (attempt === maxAttempts) {
process.stderr.write(`cf-rate-limit-retry: still rate limited after ${maxAttempts} attempts; giving up\n`);
process.exit(code);
}
const delayMs = delays[attempt - 1];
process.stderr.write(
`cf-rate-limit-retry: Cloudflare API rate limited (10429); retrying in ${Math.round(delayMs / 1000)}s (attempt ${attempt + 1} of ${maxAttempts})\n`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}