Skip to content

Commit 2c45875

Browse files
committed
ci: keep Bundle Size job green on transient GitHub comment failures
The size measurement and job summary had already succeeded on PR #1789 (run 32050847506) when the PR comment write got a 503 during a GitHub incident and failed the whole lane. --post-comment now retries 5xx / 429 / network errors (4 attempts, 1s/2s/4s backoff) on both the list and write calls. If it still fails, it prints a ::warning::, appends a note to $GITHUB_STEP_SUMMARY, and exits 0. Other 4xx (bad token, missing permissions) stay fatal.
1 parent 3020195 commit 2c45875

1 file changed

Lines changed: 66 additions & 14 deletions

File tree

‎scripts/size-report.mjs‎

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { performance } from 'node:perf_hooks';
66
import { gzipSync } from 'node:zlib';
77

88
const COMMENT_MARKER = '<!-- agent-device-size-report -->';
9+
const GITHUB_REQUEST_ATTEMPTS = 4;
10+
const GITHUB_RETRY_BASE_MS = 1000;
11+
class TransientGitHubError extends Error {}
912
const VALUE_ARGS = new Map([
1013
['--cwd', 'cwd'],
1114
['--json', 'json'],
@@ -25,7 +28,7 @@ const args = parseArgs(process.argv.slice(2));
2528
const cwd = path.resolve(args.cwd ?? process.cwd());
2629

2730
if (args.postComment) {
28-
await postGitHubComment(args.postComment, args.pr);
31+
await postGitHubCommentBestEffort(args.postComment, args.pr);
2932
process.exit(0);
3033
}
3134

@@ -361,6 +364,25 @@ function writeFile(filePath, contents) {
361364
fs.writeFileSync(filePath, contents);
362365
}
363366

367+
// The PR comment is a convenience surface: the same markdown is already in the
368+
// job summary. A GitHub outage (5xx / 429 / network error) must not fail the
369+
// job, but a real misconfiguration (bad token, missing permissions) still does.
370+
async function postGitHubCommentBestEffort(markdownPath, explicitPrNumber) {
371+
try {
372+
await postGitHubComment(markdownPath, explicitPrNumber);
373+
} catch (error) {
374+
if (!(error instanceof TransientGitHubError)) throw error;
375+
const message = `Skipping PR size comment after transient GitHub failure: ${error.message}`;
376+
process.stdout.write(`::warning::${message}\n`);
377+
appendStepSummary(`> ⚠️ ${message} The size report above is authoritative.\n`);
378+
}
379+
}
380+
381+
function appendStepSummary(text) {
382+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
383+
if (summaryPath) fs.appendFileSync(summaryPath, text);
384+
}
385+
364386
async function postGitHubComment(markdownPath, explicitPrNumber) {
365387
const config = readGitHubCommentConfig(explicitPrNumber);
366388
const body = fs.readFileSync(markdownPath, 'utf8');
@@ -407,21 +429,21 @@ function buildCommentsUrl(repository, prNumber) {
407429
}
408430

409431
async function listGitHubComments(commentsUrl, headers) {
410-
const response = await fetch(`${commentsUrl}?per_page=100`, { headers });
411-
if (!response.ok) {
412-
throw new Error(`Failed to list PR comments: ${response.status} ${await response.text()}`);
413-
}
432+
const response = await githubRequest(
433+
`${commentsUrl}?per_page=100`,
434+
{ headers },
435+
'list PR comments',
436+
);
414437
return await response.json();
415438
}
416439

417440
async function writeGitHubComment(commentsUrl, headers, body, existingUrl) {
418441
const target = commentWriteTarget(commentsUrl, existingUrl);
419-
const response = await fetch(target.url, {
420-
method: target.method,
421-
headers,
422-
body: JSON.stringify({ body }),
423-
});
424-
await assertGitHubWriteResponse(response, target.action);
442+
await githubRequest(
443+
target.url,
444+
{ method: target.method, headers, body: JSON.stringify({ body }) },
445+
`${target.action} PR comment`,
446+
);
425447
}
426448

427449
function commentWriteTarget(commentsUrl, existingUrl) {
@@ -431,8 +453,38 @@ function commentWriteTarget(commentsUrl, existingUrl) {
431453
return { url: commentsUrl, method: 'POST', action: 'create' };
432454
}
433455

434-
async function assertGitHubWriteResponse(response, action) {
435-
if (!response.ok) {
436-
throw new Error(`Failed to ${action} PR comment: ${response.status} ${await response.text()}`);
456+
// Retries 5xx / 429 / network errors with exponential backoff; any other
457+
// non-OK status is a configuration problem and throws a plain (fatal) Error.
458+
async function githubRequest(url, init, action) {
459+
let failure = '';
460+
for (let attempt = 1; attempt <= GITHUB_REQUEST_ATTEMPTS; attempt += 1) {
461+
const result = await attemptGitHubRequest(url, init, action);
462+
if (result.response) return result.response;
463+
failure = result.failure;
464+
if (attempt < GITHUB_REQUEST_ATTEMPTS) {
465+
const delayMs = GITHUB_RETRY_BASE_MS * 2 ** (attempt - 1);
466+
process.stderr.write(`${failure} (retrying in ${delayMs}ms)\n`);
467+
await new Promise((resolve) => setTimeout(resolve, delayMs));
468+
}
437469
}
470+
throw new TransientGitHubError(`${failure} after ${GITHUB_REQUEST_ATTEMPTS} attempts`);
471+
}
472+
473+
// Resolves to { response } on success or { failure } on a transient failure;
474+
// throws a plain Error on a non-transient one.
475+
async function attemptGitHubRequest(url, init, action) {
476+
let response;
477+
try {
478+
response = await fetch(url, init);
479+
} catch (error) {
480+
return { failure: `Failed to ${action}: ${error?.message ?? error}` };
481+
}
482+
if (response.ok) return { response };
483+
const failure = `Failed to ${action}: ${response.status} ${await response.text()}`;
484+
if (isTransientGitHubStatus(response.status)) return { failure };
485+
throw new Error(failure);
486+
}
487+
488+
function isTransientGitHubStatus(status) {
489+
return status === 429 || status >= 500;
438490
}

0 commit comments

Comments
 (0)