@@ -6,6 +6,9 @@ import { performance } from 'node:perf_hooks';
66import { gzipSync } from 'node:zlib' ;
77
88const COMMENT_MARKER = '<!-- agent-device-size-report -->' ;
9+ const GITHUB_REQUEST_ATTEMPTS = 4 ;
10+ const GITHUB_RETRY_BASE_MS = 1000 ;
11+ class TransientGitHubError extends Error { }
912const VALUE_ARGS = new Map ( [
1013 [ '--cwd' , 'cwd' ] ,
1114 [ '--json' , 'json' ] ,
@@ -25,7 +28,7 @@ const args = parseArgs(process.argv.slice(2));
2528const cwd = path . resolve ( args . cwd ?? process . cwd ( ) ) ;
2629
2730if ( 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+
364386async 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
409431async 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
417440async 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
427449function 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