Skip to content

Commit 2a429cd

Browse files
committed
refactor: simplify GitHub release API and remove unnecessary methods
1 parent 516b2b8 commit 2a429cd

3 files changed

Lines changed: 68 additions & 755 deletions

File tree

src/releases/github.ts

Lines changed: 44 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -208,16 +208,17 @@ export async function downloadGitHubRelease(
208208

209209
/**
210210
* Download a specific release asset.
211+
* Supports pattern matching for dynamic asset discovery.
211212
*
212213
* @param tag - Release tag name
213-
* @param assetName - Asset name to download
214+
* @param assetPattern - Asset name or pattern (glob string, prefix/suffix object, or RegExp)
214215
* @param outputPath - Path to write the downloaded file
215216
* @param repoConfig - Repository configuration (owner/repo)
216217
* @param options - Additional options
217218
*/
218219
export async function downloadReleaseAsset(
219220
tag: string,
220-
assetName: string,
221+
assetPattern: string | AssetPattern,
221222
outputPath: string,
222223
repoConfig: RepoConfig,
223224
options: { quiet?: boolean } = {},
@@ -228,13 +229,15 @@ export async function downloadReleaseAsset(
228229
// Get the browser_download_url for the asset.
229230
const downloadUrl = await getReleaseAssetUrl(
230231
tag,
231-
assetName,
232+
assetPattern,
232233
{ owner, repo },
233234
{ quiet },
234235
)
235236

236237
if (!downloadUrl) {
237-
throw new Error(`Asset ${assetName} not found in release ${tag}`)
238+
const patternDesc =
239+
typeof assetPattern === 'string' ? assetPattern : 'matching pattern'
240+
throw new Error(`Asset ${patternDesc} not found in release ${tag}`)
238241
}
239242

240243
const path = getPath()
@@ -287,16 +290,6 @@ export function getAuthHeaders(): Record<string, string> {
287290
*/
288291
export type AssetPattern = string | { prefix: string; suffix: string } | RegExp
289292

290-
/**
291-
* Result of finding a release asset.
292-
*/
293-
export interface FindReleaseAssetResult {
294-
/** The release tag name. */
295-
tag: string
296-
/** The matching asset name. */
297-
assetName: string
298-
}
299-
300293
/**
301294
* Create a matcher function for a pattern using picomatch for glob patterns
302295
* or simple prefix/suffix matching for object patterns.
@@ -334,78 +327,25 @@ function createMatcher(
334327
}
335328

336329
/**
337-
* Find a release asset matching a pattern in the latest release.
338-
* Searches for the first release matching the tool prefix,
339-
* then finds the first asset matching the provided pattern.
330+
* Get latest release tag matching a tool prefix.
331+
* Optionally filter by releases containing a matching asset.
340332
*
341-
* @param toolPrefix - Tool name prefix to search for (e.g., 'yoga-layout-')
342-
* @param assetPattern - Pattern to match asset names (glob string, prefix/suffix object, or RegExp)
333+
* @param toolPrefix - Tool name prefix to search for (e.g., 'node-smol-')
343334
* @param repoConfig - Repository configuration (owner/repo)
344335
* @param options - Additional options
345-
* @returns Result with tag and asset name, or null if not found
346-
*
347-
* @example
348-
* ```ts
349-
* // Find yoga-sync asset with glob pattern
350-
* const result = await findReleaseAsset(
351-
* 'yoga-layout-',
352-
* 'yoga-sync-*.mjs',
353-
* { owner: 'SocketDev', repo: 'socket-btm' }
354-
* )
355-
* // result = { tag: 'yoga-layout-2024-01-15-abc123', assetName: 'yoga-sync-2024-01-15-abc123.mjs' }
356-
* ```
357-
*
358-
* @example
359-
* ```ts
360-
* // Find models tar.gz with glob pattern
361-
* const result = await findReleaseAsset(
362-
* 'models-',
363-
* 'models-*.tar.gz',
364-
* { owner: 'SocketDev', repo: 'socket-btm' }
365-
* )
366-
* ```
367-
*
368-
* @example
369-
* ```ts
370-
* // Find asset with glob braces pattern
371-
* const result = await findReleaseAsset(
372-
* 'yoga-layout-',
373-
* 'yoga-{sync,layout}-*.{mjs,js}',
374-
* { owner: 'SocketDev', repo: 'socket-btm' }
375-
* )
376-
* ```
377-
*
378-
* @example
379-
* ```ts
380-
* // Find asset with object pattern (backward compatible)
381-
* const result = await findReleaseAsset(
382-
* 'yoga-layout-',
383-
* { prefix: 'yoga-sync-', suffix: '.mjs' },
384-
* { owner: 'SocketDev', repo: 'socket-btm' }
385-
* )
386-
* ```
387-
*
388-
* @example
389-
* ```ts
390-
* // Find models tar.gz with regex
391-
* const result = await findReleaseAsset(
392-
* 'models-',
393-
* /^models-\d{4}-\d{2}-\d{2}-.+\.tar\.gz$/,
394-
* { owner: 'SocketDev', repo: 'socket-btm' }
395-
* )
396-
* ```
336+
* @param options.assetPattern - Optional pattern to filter releases by matching asset
337+
* @returns Latest release tag or null if not found
397338
*/
398-
export async function findReleaseAsset(
339+
export async function getLatestRelease(
399340
toolPrefix: string,
400-
assetPattern: AssetPattern,
401341
repoConfig: RepoConfig,
402-
options: { quiet?: boolean } = {},
403-
): Promise<FindReleaseAssetResult | null> {
342+
options: { assetPattern?: AssetPattern; quiet?: boolean } = {},
343+
): Promise<string | null> {
344+
const { assetPattern, quiet = false } = options
404345
const { owner, repo } = repoConfig
405-
const { quiet = false } = options
406346

407-
// Create matcher function for the pattern.
408-
const isMatch = createMatcher(assetPattern)
347+
// Create matcher function if pattern provided.
348+
const isMatch = assetPattern ? createMatcher(assetPattern) : undefined
409349

410350
return await pRetry(
411351
async () => {
@@ -430,87 +370,20 @@ export async function findReleaseAsset(
430370
continue
431371
}
432372

433-
// Find matching asset in this release using the matcher function.
434-
const matchingAsset = assets.find((a: { name: string }) =>
435-
isMatch(a.name),
436-
)
437-
438-
if (matchingAsset) {
439-
if (!quiet) {
440-
logger.info(`Found release: ${tag}`)
441-
logger.info(`Found asset: ${matchingAsset.name}`)
442-
}
443-
return {
444-
assetName: matchingAsset.name,
445-
tag,
373+
// If asset pattern provided, check if release has matching asset.
374+
if (isMatch) {
375+
const hasMatchingAsset = assets.some((a: { name: string }) =>
376+
isMatch(a.name),
377+
)
378+
if (!hasMatchingAsset) {
379+
continue
446380
}
447381
}
448-
}
449382

450-
// No matching release or asset found.
451-
if (!quiet) {
452-
logger.info(`No ${toolPrefix} release with matching asset found`)
453-
}
454-
return null
455-
},
456-
{
457-
...RETRY_CONFIG,
458-
onRetry: (attempt, error) => {
459383
if (!quiet) {
460-
logger.info(
461-
`Retry attempt ${attempt + 1}/${RETRY_CONFIG.retries + 1} for release asset search...`,
462-
)
463-
logger.warn(
464-
`Attempt ${attempt + 1}/${RETRY_CONFIG.retries + 1} failed: ${error instanceof Error ? error.message : String(error)}`,
465-
)
466-
}
467-
return undefined
468-
},
469-
},
470-
)
471-
}
472-
473-
/**
474-
* Get latest release tag matching a tool prefix.
475-
*
476-
* @param toolPrefix - Tool name prefix to search for (e.g., 'node-smol-')
477-
* @param repoConfig - Repository configuration (owner/repo)
478-
* @param options - Additional options
479-
* @returns Latest release tag or null if not found
480-
*/
481-
export async function getLatestRelease(
482-
toolPrefix: string,
483-
repoConfig: RepoConfig,
484-
options: { quiet?: boolean } = {},
485-
): Promise<string | null> {
486-
const { owner, repo } = repoConfig
487-
const { quiet = false } = options
488-
489-
return await pRetry(
490-
async () => {
491-
// Fetch recent releases (100 should cover all tool releases).
492-
const response = await httpRequest(
493-
`https://api.github.com/repos/${owner}/${repo}/releases?per_page=100`,
494-
{
495-
headers: getAuthHeaders(),
496-
},
497-
)
498-
499-
if (!response.ok) {
500-
throw new Error(`Failed to fetch releases: ${response.status}`)
501-
}
502-
503-
const releases = JSON.parse(response.body.toString('utf8'))
504-
505-
// Find the first release matching the tool prefix.
506-
for (const release of releases) {
507-
const { tag_name: tag } = release
508-
if (tag.startsWith(toolPrefix)) {
509-
if (!quiet) {
510-
logger.info(`Found release: ${tag}`)
511-
}
512-
return tag
384+
logger.info(`Found release: ${tag}`)
513385
}
386+
return tag
514387
}
515388

516389
// No matching release found.
@@ -538,22 +411,31 @@ export async function getLatestRelease(
538411

539412
/**
540413
* Get download URL for a specific release asset.
414+
* Supports pattern matching for dynamic asset discovery.
541415
*
542416
* @param tag - Release tag name
543-
* @param assetName - Asset name to download
417+
* @param assetPattern - Asset name or pattern (glob string, prefix/suffix object, or RegExp)
544418
* @param repoConfig - Repository configuration (owner/repo)
545419
* @param options - Additional options
546420
* @returns Browser download URL for the asset
547421
*/
548422
export async function getReleaseAssetUrl(
549423
tag: string,
550-
assetName: string,
424+
assetPattern: string | AssetPattern,
551425
repoConfig: RepoConfig,
552426
options: { quiet?: boolean } = {},
553427
): Promise<string | null> {
554428
const { owner, repo } = repoConfig
555429
const { quiet = false } = options
556430

431+
// Create matcher function for the pattern.
432+
const isMatch =
433+
typeof assetPattern === 'string' &&
434+
!assetPattern.includes('*') &&
435+
!assetPattern.includes('{')
436+
? (input: string) => input === assetPattern
437+
: createMatcher(assetPattern as AssetPattern)
438+
557439
return await pRetry(
558440
async () => {
559441
const response = await httpRequest(
@@ -570,16 +452,18 @@ export async function getReleaseAssetUrl(
570452
const release = JSON.parse(response.body.toString('utf8'))
571453

572454
// Find the matching asset.
573-
const asset = release.assets.find(
574-
(a: { name: string }) => a.name === assetName,
455+
const asset = release.assets.find((a: { name: string }) =>
456+
isMatch(a.name),
575457
)
576458

577459
if (!asset) {
578-
throw new Error(`Asset ${assetName} not found in release ${tag}`)
460+
const patternDesc =
461+
typeof assetPattern === 'string' ? assetPattern : 'matching pattern'
462+
throw new Error(`Asset ${patternDesc} not found in release ${tag}`)
579463
}
580464

581465
if (!quiet) {
582-
logger.info(`Found asset: ${assetName}`)
466+
logger.info(`Found asset: ${asset.name}`)
583467
}
584468

585469
return asset.browser_download_url

src/releases/socket-btm.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
type AssetPattern,
1616
downloadGitHubRelease,
1717
type DownloadGitHubReleaseConfig,
18-
findReleaseAsset,
18+
getLatestRelease,
19+
getReleaseAssetUrl,
1920
SOCKET_BTM_REPO,
2021
} from './github.js'
2122

@@ -176,20 +177,32 @@ export async function downloadSocketBtmRelease(
176177
)
177178
}
178179

179-
// Find matching asset in latest release.
180-
const result = await findReleaseAsset(
181-
toolPrefix,
180+
// Find latest release with matching asset.
181+
resolvedTag = await getLatestRelease(toolPrefix, SOCKET_BTM_REPO, {
182+
assetPattern: asset,
183+
quiet,
184+
})
185+
186+
if (!resolvedTag) {
187+
throw new Error(`No ${tool} release with matching asset pattern found`)
188+
}
189+
190+
// Get the matching asset URL (which will give us the asset name).
191+
const assetUrl = await getReleaseAssetUrl(
192+
resolvedTag,
182193
asset,
183-
{ owner: SOCKET_BTM_REPO.owner, repo: SOCKET_BTM_REPO.repo },
184-
{ quiet },
194+
SOCKET_BTM_REPO,
195+
{
196+
quiet,
197+
},
185198
)
186199

187-
if (!result) {
188-
throw new Error(`No ${tool} release with matching asset pattern found`)
200+
if (!assetUrl) {
201+
throw new Error(`No matching asset found in release ${resolvedTag}`)
189202
}
190203

191-
resolvedAsset = result.assetName
192-
resolvedTag = result.tag
204+
// Extract asset name from URL.
205+
resolvedAsset = assetUrl.split('/').pop() || asset.toString()
193206
}
194207

195208
// Default output to resolved asset name if not provided

0 commit comments

Comments
 (0)