@@ -14,6 +14,7 @@ const GITHUB_RELEASES_URL = `https://api.github.com/repos/${REPO_OWNER}/${REPO_N
1414const GITHUB_RELEASE_TAG_URL = `https://api.github.com/repos/${ REPO_OWNER } /${ REPO_NAME } /releases/tags` ;
1515const TIMEOUT_MS = 60000 ;
1616const MAX_RETRIES = 2 ;
17+ const RETRYABLE_STATUS = new Set ( [ 429 , 500 , 502 , 503 , 504 ] ) ;
1718
1819export interface ReleaseInfo {
1920 tag_name : string ;
@@ -31,12 +32,62 @@ export interface UpdateStatus {
3132 releaseInfo ?: ReleaseInfo ;
3233}
3334
35+ function getGithubHeaders ( ) : Record < string , string > {
36+ const headers : Record < string , string > = { 'User-Agent' : 'cloudsqlctl/upgrade' } ;
37+ const token = process . env . CLOUDSQLCTL_GITHUB_TOKEN || process . env . GITHUB_TOKEN ;
38+ if ( token ) {
39+ headers . Authorization = `Bearer ${ token } ` ;
40+ }
41+ return headers ;
42+ }
43+
44+ function getRateLimitMessage ( error : unknown ) : string | null {
45+ if ( axios . isAxiosError ( error ) && error . response ) {
46+ const remaining = error . response . headers [ 'x-ratelimit-remaining' ] ;
47+ const reset = error . response . headers [ 'x-ratelimit-reset' ] ;
48+ if ( remaining === '0' && reset ) {
49+ const resetTime = new Date ( Number ( reset ) * 1000 ) . toISOString ( ) ;
50+ return `GitHub API rate limit exceeded. Resets at ${ resetTime } .` ;
51+ }
52+ }
53+ return null ;
54+ }
55+
56+ function shouldRetry ( error : unknown ) : boolean {
57+ if ( axios . isAxiosError ( error ) ) {
58+ if ( ! error . response ) return true ;
59+ return RETRYABLE_STATUS . has ( error . response . status ) ;
60+ }
61+ return false ;
62+ }
63+
64+ async function githubGet < T > ( url : string ) {
65+ let attempt = 0 ;
66+ while ( attempt <= MAX_RETRIES ) {
67+ try {
68+ return await axios . get < T > ( url , {
69+ timeout : TIMEOUT_MS ,
70+ headers : getGithubHeaders ( )
71+ } ) ;
72+ } catch ( error ) {
73+ attempt ++ ;
74+ const rateLimit = getRateLimitMessage ( error ) ;
75+ if ( rateLimit ) {
76+ logger . warn ( rateLimit ) ;
77+ }
78+ if ( attempt > MAX_RETRIES || ! shouldRetry ( error ) ) {
79+ throw error ;
80+ }
81+ const delayMs = 1000 * attempt ;
82+ await new Promise ( resolve => setTimeout ( resolve , delayMs ) ) ;
83+ }
84+ }
85+ throw new Error ( 'GitHub API request failed after retries' ) ;
86+ }
87+
3488export async function getLatestRelease ( ) : Promise < ReleaseInfo > {
3589 try {
36- const response = await axios . get ( GITHUB_API_URL , {
37- timeout : TIMEOUT_MS ,
38- headers : { 'User-Agent' : 'cloudsqlctl/upgrade' }
39- } ) ;
90+ const response = await githubGet < ReleaseInfo > ( GITHUB_API_URL ) ;
4091 return response . data ;
4192 } catch ( error ) {
4293 logger . error ( 'Failed to fetch latest release info' , error ) ;
@@ -50,10 +101,7 @@ function normalizeTag(tag: string): string {
50101
51102export async function getReleaseByTag ( tag : string ) : Promise < ReleaseInfo > {
52103 try {
53- const response = await axios . get ( `${ GITHUB_RELEASE_TAG_URL } /${ normalizeTag ( tag ) } ` , {
54- timeout : TIMEOUT_MS ,
55- headers : { 'User-Agent' : 'cloudsqlctl/upgrade' }
56- } ) ;
104+ const response = await githubGet < ReleaseInfo > ( `${ GITHUB_RELEASE_TAG_URL } /${ normalizeTag ( tag ) } ` ) ;
57105 return response . data ;
58106 } catch ( error ) {
59107 logger . error ( 'Failed to fetch release by tag' , error ) ;
@@ -63,10 +111,7 @@ export async function getReleaseByTag(tag: string): Promise<ReleaseInfo> {
63111
64112export async function getLatestPrerelease ( ) : Promise < ReleaseInfo > {
65113 try {
66- const response = await axios . get ( GITHUB_RELEASES_URL , {
67- timeout : TIMEOUT_MS ,
68- headers : { 'User-Agent' : 'cloudsqlctl/upgrade' }
69- } ) ;
114+ const response = await githubGet < ReleaseInfo [ ] > ( GITHUB_RELEASES_URL ) ;
70115 const releases = Array . isArray ( response . data ) ? response . data : [ ] ;
71116 const prerelease = releases . find ( ( r : { prerelease ?: boolean ; draft ?: boolean } ) => r . prerelease && ! r . draft ) ;
72117 if ( ! prerelease ) {
@@ -123,7 +168,8 @@ export async function fetchSha256Sums(release: ReleaseInfo): Promise<Map<string,
123168
124169 const response = await axios . get ( checksumAsset . browser_download_url , {
125170 responseType : 'text' ,
126- timeout : TIMEOUT_MS
171+ timeout : TIMEOUT_MS ,
172+ headers : getGithubHeaders ( )
127173 } ) ;
128174
129175 const sums = new Map < string , string > ( ) ;
0 commit comments