Skip to content

Commit b464ddb

Browse files
committed
feat(releases): add findReleaseAsset helper for pattern-based asset discovery
Add new findReleaseAsset helper function to support dynamic discovery of socket-btm release assets with timestamped names. - Add findReleaseAsset function to query releases and find matching assets - Support both prefix/suffix patterns and RegExp patterns - Return tag and asset name for the first matching asset - Include proper error handling and retry logic with exponential backoff - Add comprehensive JSDoc documentation with examples This enables socket-cli build scripts to dynamically discover assets without hardcoding asset names, making the build process more maintainable.
1 parent b82c579 commit b464ddb

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

src/releases/github.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,136 @@ export function getAuthHeaders(): Record<string, string> {
266266
return headers
267267
}
268268

269+
/**
270+
* Pattern for matching release assets.
271+
* Can be either a prefix/suffix pair or a RegExp.
272+
*/
273+
export type AssetPattern = { prefix: string; suffix: string } | RegExp
274+
275+
/**
276+
* Result of finding a release asset.
277+
*/
278+
export interface FindReleaseAssetResult {
279+
/** The release tag name. */
280+
tag: string
281+
/** The matching asset name. */
282+
assetName: string
283+
}
284+
285+
/**
286+
* Find a release asset matching a pattern in the latest release.
287+
* Searches for the first release matching the tool prefix,
288+
* then finds the first asset matching the provided pattern.
289+
*
290+
* @param toolPrefix - Tool name prefix to search for (e.g., 'yoga-layout-')
291+
* @param assetPattern - Pattern to match asset names (prefix/suffix or RegExp)
292+
* @param repoConfig - Repository configuration (owner/repo)
293+
* @param options - Additional options
294+
* @returns Result with tag and asset name, or null if not found
295+
*
296+
* @example
297+
* ```ts
298+
* // Find yoga-sync asset with timestamped name
299+
* const result = await findReleaseAsset(
300+
* 'yoga-layout-',
301+
* { prefix: 'yoga-sync-', suffix: '.mjs' },
302+
* { owner: 'SocketDev', repo: 'socket-btm' }
303+
* )
304+
* // result = { tag: 'yoga-layout-2024-01-15-abc123', assetName: 'yoga-sync-2024-01-15-abc123.mjs' }
305+
* ```
306+
*
307+
* @example
308+
* ```ts
309+
* // Find models tar.gz with regex
310+
* const result = await findReleaseAsset(
311+
* 'models-',
312+
* /^models-\d{4}-\d{2}-\d{2}-.+\.tar\.gz$/,
313+
* { owner: 'SocketDev', repo: 'socket-btm' }
314+
* )
315+
* ```
316+
*/
317+
export async function findReleaseAsset(
318+
toolPrefix: string,
319+
assetPattern: AssetPattern,
320+
repoConfig: RepoConfig,
321+
options: { quiet?: boolean } = {},
322+
): Promise<FindReleaseAssetResult | null> {
323+
const { owner, repo } = repoConfig
324+
const { quiet = false } = options
325+
326+
return await pRetry(
327+
async () => {
328+
// Fetch recent releases (100 should cover all tool releases).
329+
const response = await httpRequest(
330+
`https://api.github.com/repos/${owner}/${repo}/releases?per_page=100`,
331+
{
332+
headers: getAuthHeaders(),
333+
},
334+
)
335+
336+
if (!response.ok) {
337+
throw new Error(`Failed to fetch releases: ${response.status}`)
338+
}
339+
340+
const releases = JSON.parse(response.body.toString('utf8'))
341+
342+
// Find the first release matching the tool prefix.
343+
for (const release of releases) {
344+
const { assets, tag_name: tag } = release
345+
if (!tag.startsWith(toolPrefix)) {
346+
continue
347+
}
348+
349+
// Find matching asset in this release.
350+
let matchingAsset: { name: string } | undefined
351+
352+
if (assetPattern instanceof RegExp) {
353+
matchingAsset = assets.find((a: { name: string }) =>
354+
assetPattern.test(a.name),
355+
)
356+
} else {
357+
const { prefix, suffix } = assetPattern
358+
matchingAsset = assets.find(
359+
(a: { name: string }) =>
360+
a.name.startsWith(prefix) && a.name.endsWith(suffix),
361+
)
362+
}
363+
364+
if (matchingAsset) {
365+
if (!quiet) {
366+
logger.info(`Found release: ${tag}`)
367+
logger.info(`Found asset: ${matchingAsset.name}`)
368+
}
369+
return {
370+
assetName: matchingAsset.name,
371+
tag,
372+
}
373+
}
374+
}
375+
376+
// No matching release or asset found.
377+
if (!quiet) {
378+
logger.info(`No ${toolPrefix} release with matching asset found`)
379+
}
380+
return null
381+
},
382+
{
383+
...RETRY_CONFIG,
384+
onRetry: (attempt, error) => {
385+
if (!quiet) {
386+
logger.info(
387+
`Retry attempt ${attempt + 1}/${RETRY_CONFIG.retries + 1} for release asset search...`,
388+
)
389+
logger.warn(
390+
`Attempt ${attempt + 1}/${RETRY_CONFIG.retries + 1} failed: ${error instanceof Error ? error.message : String(error)}`,
391+
)
392+
}
393+
return undefined
394+
},
395+
},
396+
)
397+
}
398+
269399
/**
270400
* Get latest release tag matching a tool prefix.
271401
*

0 commit comments

Comments
 (0)