22 * Shared utilities for fetching GitHub releases.
33 */
44
5+ import { createTtlCache } from '@socketsecurity/lib/cache-with-ttl'
56import { safeMkdir } from '@socketsecurity/lib/fs'
67import { httpDownload , httpRequest } from '@socketsecurity/lib/http-request'
78import { getDefaultLogger } from '@socketsecurity/lib/logger'
@@ -12,6 +13,13 @@ const logger = getDefaultLogger()
1213const OWNER = 'SocketDev'
1314const REPO = 'socket-btm'
1415
16+ // Cache GitHub API responses for 1 hour to avoid rate limiting.
17+ const cache = createTtlCache ( {
18+ memoize : true ,
19+ prefix : 'github-releases' ,
20+ ttl : 60 * 60 * 1000 , // 1 hour.
21+ } )
22+
1523/**
1624 * Get GitHub authentication headers if token is available.
1725 *
@@ -38,52 +46,56 @@ function getAuthHeaders() {
3846 * @returns {Promise<string|null> } - Latest release tag or null if not found.
3947 */
4048export async function getLatestRelease ( tool , { quiet = false } = { } ) {
41- return await pRetry (
42- async ( ) => {
43- const response = await httpRequest (
44- `https://api.github.com/repos/${ OWNER } /${ REPO } /releases?per_page=100` ,
45- {
46- headers : getAuthHeaders ( ) ,
47- } ,
48- )
49-
50- if ( ! response . ok ) {
51- throw new Error ( `Failed to fetch releases: ${ response . status } ` )
52- }
49+ const cacheKey = `latest-release:${ tool } `
50+
51+ return await cache . getOrFetch ( cacheKey , async ( ) => {
52+ return await pRetry (
53+ async ( ) => {
54+ const response = await httpRequest (
55+ `https://api.github.com/repos/${ OWNER } /${ REPO } /releases?per_page=100` ,
56+ {
57+ headers : getAuthHeaders ( ) ,
58+ } ,
59+ )
60+
61+ if ( ! response . ok ) {
62+ throw new Error ( `Failed to fetch releases: ${ response . status } ` )
63+ }
5364
54- const releases = JSON . parse ( response . body )
65+ const releases = JSON . parse ( response . body )
5566
56- // Find the first release matching the tool prefix.
57- for ( const release of releases ) {
58- const { tag_name : tag } = release
59- if ( tag . startsWith ( `${ tool } -` ) ) {
60- if ( ! quiet ) {
61- logger . info ( ` Found release: ${ tag } ` )
67+ // Find the first release matching the tool prefix.
68+ for ( const release of releases ) {
69+ const { tag_name : tag } = release
70+ if ( tag . startsWith ( `${ tool } -` ) ) {
71+ if ( ! quiet ) {
72+ logger . info ( ` Found release: ${ tag } ` )
73+ }
74+ return tag
6275 }
63- return tag
6476 }
65- }
66-
67- // No matching release found in the list.
68- if ( ! quiet ) {
69- logger . info ( ` No ${ tool } release found in latest 100 releases` )
70- }
71- return null
72- } ,
73- {
74- backoffFactor : 1 ,
75- baseDelayMs : 5000 ,
76- onRetry : ( attempt , error ) => {
77+
78+ // No matching release found in the list.
7779 if ( ! quiet ) {
78- logger . info (
79- ` Retry attempt ${ attempt + 1 } /3 for ${ tool } release list...` ,
80- )
81- logger . warn ( ` Attempt ${ attempt + 1 } /3 failed: ${ error . message } ` )
80+ logger . info ( ` No ${ tool } release found in latest 100 releases` )
8281 }
82+ return null
8383 } ,
84- retries : 2 ,
85- } ,
86- )
84+ {
85+ backoffFactor : 1 ,
86+ baseDelayMs : 5000 ,
87+ onRetry : ( attempt , error ) => {
88+ if ( ! quiet ) {
89+ logger . info (
90+ ` Retry attempt ${ attempt + 1 } /3 for ${ tool } release list...` ,
91+ )
92+ logger . warn ( ` Attempt ${ attempt + 1 } /3 failed: ${ error . message } ` )
93+ }
94+ } ,
95+ retries : 2 ,
96+ } ,
97+ )
98+ } )
8799}
88100
89101/**
@@ -103,46 +115,50 @@ export async function getReleaseAssetUrl(
103115 assetName ,
104116 { quiet = false } = { } ,
105117) {
106- return await pRetry (
107- async ( ) => {
108- const response = await httpRequest (
109- `https://api.github.com/repos/${ OWNER } /${ REPO } /releases/tags/${ tag } ` ,
110- {
111- headers : getAuthHeaders ( ) ,
112- } ,
113- )
114-
115- if ( ! response . ok ) {
116- throw new Error ( `Failed to fetch release ${ tag } : ${ response . status } ` )
117- }
118-
119- const release = JSON . parse ( response . body )
118+ const cacheKey = `asset-url:${ tag } :${ assetName } `
119+
120+ return await cache . getOrFetch ( cacheKey , async ( ) => {
121+ return await pRetry (
122+ async ( ) => {
123+ const response = await httpRequest (
124+ `https://api.github.com/repos/${ OWNER } /${ REPO } /releases/tags/${ tag } ` ,
125+ {
126+ headers : getAuthHeaders ( ) ,
127+ } ,
128+ )
129+
130+ if ( ! response . ok ) {
131+ throw new Error ( `Failed to fetch release ${ tag } : ${ response . status } ` )
132+ }
120133
121- // Find the matching asset.
122- const asset = release . assets . find ( a => a . name === assetName )
134+ const release = JSON . parse ( response . body )
123135
124- if ( ! asset ) {
125- throw new Error ( `Asset ${ assetName } not found in release ${ tag } ` )
126- }
136+ // Find the matching asset.
137+ const asset = release . assets . find ( a => a . name === assetName )
127138
128- if ( ! quiet ) {
129- logger . info ( ` Found asset: ${ assetName } `)
130- }
139+ if ( ! asset ) {
140+ throw new Error ( `Asset ${ assetName } not found in release ${ tag } `)
141+ }
131142
132- return asset . browser_download_url
133- } ,
134- {
135- backoffFactor : 1 ,
136- baseDelayMs : 5000 ,
137- onRetry : ( attempt , error ) => {
138143 if ( ! quiet ) {
139- logger . info ( ` Retry attempt ${ attempt + 1 } /3 for asset URL...` )
140- logger . warn ( ` Attempt ${ attempt + 1 } /3 failed: ${ error . message } ` )
144+ logger . info ( ` Found asset: ${ assetName } ` )
141145 }
146+
147+ return asset . browser_download_url
142148 } ,
143- retries : 2 ,
144- } ,
145- )
149+ {
150+ backoffFactor : 1 ,
151+ baseDelayMs : 5000 ,
152+ onRetry : ( attempt , error ) => {
153+ if ( ! quiet ) {
154+ logger . info ( ` Retry attempt ${ attempt + 1 } /3 for asset URL...` )
155+ logger . warn ( ` Attempt ${ attempt + 1 } /3 failed: ${ error . message } ` )
156+ }
157+ } ,
158+ retries : 2 ,
159+ } ,
160+ )
161+ } )
146162}
147163
148164/**
0 commit comments