Skip to content

Commit 3f6a7a6

Browse files
committed
Reapply "fix: add 1-hour TTL cache for GitHub API requests to avoid rate limiting"
This reverts commit 7fb6023.
1 parent 7fb6023 commit 3f6a7a6

File tree

2 files changed

+98
-87
lines changed

2 files changed

+98
-87
lines changed

packages/build-infra/lib/github-releases.mjs

Lines changed: 88 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Shared utilities for fetching GitHub releases.
33
*/
44

5+
import { createTtlCache } from '@socketsecurity/lib/cache-with-ttl'
56
import { safeMkdir } from '@socketsecurity/lib/fs'
67
import { httpDownload, httpRequest } from '@socketsecurity/lib/http-request'
78
import { getDefaultLogger } from '@socketsecurity/lib/logger'
@@ -12,6 +13,13 @@ const logger = getDefaultLogger()
1213
const OWNER = 'SocketDev'
1314
const 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
*/
4048
export 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
/**

packages/cli/src/commands/patch/cmd-patch.mts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,16 @@ async function run(
6262
process.exitCode = 1
6363

6464
// Forward all arguments to socket-patch via DLX.
65-
const { spawnPromise } = await spawnSocketPatchDlx([...argv])
65+
const { spawnPromise } = await spawnSocketPatchDlx([...argv], {
66+
stdio: 'inherit',
67+
})
6668

67-
// Handle exit codes and signals using event-based pattern.
68-
// See https://nodejs.org/api/child_process.html#event-exit.
69-
spawnPromise.process.on(
70-
'exit',
71-
(code: number | null, signalName: NodeJS.Signals | null) => {
72-
if (signalName) {
73-
process.kill(process.pid, signalName)
74-
} else if (typeof code === 'number') {
75-
// eslint-disable-next-line n/no-process-exit
76-
process.exit(code)
77-
}
78-
},
79-
)
69+
// Wait for the spawn to complete and set exit code.
70+
const result = await spawnPromise
8071

81-
await spawnPromise
72+
if (result.code !== null && result.code !== 0) {
73+
process.exitCode = result.code
74+
} else if (result.code === 0) {
75+
process.exitCode = 0
76+
}
8277
}

0 commit comments

Comments
 (0)