diff --git a/README.md b/README.md index e81ea14..dbaba7b 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ The primary goals are, in order of priority: This action only supports installing from releases where the release: - is tagged with the full `{major}.{minor}.{patch}` semantic version -- contains raw binary assets (archives not supported) +- contains raw binary assets or zip archives (zip archives must contain exactly one binary file) - assets are labeled with the binary name and [target triple] in the format `-` + - also supports assets labeled with underscore format: `__` (e.g., `fossa_linux_amd64`) You can create compatible releases with [semantic-release], using a workflow like [semantic-release-action/rust]. @@ -63,6 +64,16 @@ Install a binary from a release with multiple binaries available: EricCrosson/future-tools/flux-capacitor@v1 ``` +Install a binary from a zip archive (the zip must contain exactly one binary file): + +```yaml +- name: Install FOSSA CLI + uses: EricCrosson/install-github-release-binary@v2 + with: + targets: | + fossas/fossa-cli@v3.11.7:sha256-d6f73d3da1cc7727610dd3f2c1a6021aeb23516f74b6f031e91deb31eba34f2b +``` + ## Inputs | Input Parameter | Required | Description | diff --git a/action.yml b/action.yml index 2551d19..b1dc45d 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,6 @@ --- name: Install GitHub Release binary -description: Install a binary from a GitHub Release +description: Install a binary from a GitHub Release (supports both direct binaries and zip archives) author: Eric Crosson branding: icon: arrow-down diff --git a/dist/index.js b/dist/index.js index 7a7fa98..bd48787 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4747,7 +4747,7 @@ var require_tool_cache = __commonJS({ }); } exports2.extractXar = extractXar; - function extractZip(file, dest) { + function extractZip2(file, dest) { return __awaiter(this, void 0, void 0, function* () { if (!file) { throw new Error("parameter 'file' is required"); @@ -4761,7 +4761,7 @@ var require_tool_cache = __commonJS({ return dest; }); } - exports2.extractZip = extractZip; + exports2.extractZip = extractZip2; function extractZipWin(file, dest) { return __awaiter(this, void 0, void 0, function* () { const escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, ""); @@ -10001,10 +10001,16 @@ var ALL_TARGET_TRIPLES = [ "x86_64-unknown-linux-musl" ]; var TARGET_DUPLES = [ + // Hyphenated versions "linux-amd64", "linux-arm64", "darwin-amd64", - "darwin-arm64" + "darwin-arm64", + // Underscore versions + "linux_amd64", + "linux_arm64", + "darwin_amd64", + "darwin_arm64" ]; function architectureLabel(arch2) { switch (arch2) { @@ -10053,6 +10059,18 @@ function getTargetDuple(arch2, platform2) { ); } } +function getTargetDupleUnderscore(arch2, platform2) { + switch (platform2) { + case "darwin": + return arch2 === "arm64" ? "darwin_arm64" : "darwin_amd64"; + case "linux": + return arch2 === "arm64" ? "linux_arm64" : "linux_amd64"; + default: + throw new Error( + `Unsupported platform ${platform2} for target duple conversion` + ); + } +} function stripTargetTriple(value) { if (ALL_TARGET_TRIPLES.find((targetTriple) => targetTriple === value)) { return none(); @@ -10068,7 +10086,10 @@ function stripTargetTriple(value) { return some(strippedTriple); } const strippedDuple = TARGET_DUPLES.reduce( - (value2, duple) => value2.replace(new RegExp(`-${duple}$`), ""), + (value2, duple) => { + const pattern = new RegExp(`[-_]${duple}$`); + return value2.replace(pattern, ""); + }, value ); if (strippedDuple !== value) { @@ -10142,54 +10163,75 @@ async function findExactSemanticVersionTag(octokit, slug, target) { `Expected to find an exact semantic version tag matching ${target} for ${slug.owner}/${slug.repository}` ); } -function findMatchingReleaseAssetMetadata(releaseMetadata, slug, binaryName, tag, targetTriple, targetDuple) { +function findMatchingReleaseAssetMetadata(releaseMetadata, slug, binaryName, tag, targetTriple, targetDuple, targetDupleUnderscore) { if (isSome(binaryName)) { const targetLabelTraditional = `${binaryName.value}-${targetTriple}`; const targetLabelDuple = `${binaryName.value}-${targetDuple}`; + const targetLabelDupleUnderscore = targetDupleUnderscore ? `${binaryName.value}_${targetDupleUnderscore}` : ""; const asset2 = releaseMetadata.data.assets.find((asset3) => { if (typeof asset3.label === "string") { - if (asset3.label === targetLabelTraditional || asset3.label === targetLabelDuple) { + if (asset3.label === targetLabelTraditional || asset3.label === targetLabelDuple || targetLabelDupleUnderscore && asset3.label === targetLabelDupleUnderscore) { return true; } } if (typeof asset3.name === "string") { - if (asset3.name === targetLabelTraditional || asset3.name === targetLabelDuple) { + if (asset3.name === targetLabelTraditional || asset3.name === targetLabelDuple || targetLabelDupleUnderscore && asset3.name === targetLabelDupleUnderscore) { return true; } } return false; }); if (asset2 === void 0) { + const formats = [targetLabelTraditional, targetLabelDuple]; + if (targetDupleUnderscore) { + formats.push(targetLabelDupleUnderscore); + } throw new Error( - `Expected to find asset in release ${slug.owner}/${slug.repository}@${tag} with label or name ${targetLabelTraditional} or ${targetLabelDuple}` + `Expected to find asset in release ${slug.owner}/${slug.repository}@${tag} with label or name ${formats.join(" or ")}` ); } return { binaryName, - url: asset2.url + url: asset2.url, + name: asset2.name || "" }; } const matchingAssets = releaseMetadata.data.assets.filter((asset2) => { + const endsWithPlatform = (name) => { + const filenameWithoutExt = name.replace(/\.[^.]+$/, ""); + return ( + // Traditional formats with platform at the end + filenameWithoutExt.endsWith(targetTriple) || filenameWithoutExt.endsWith(targetDuple) || targetDupleUnderscore && filenameWithoutExt.endsWith(targetDupleUnderscore) + ); + }; if (typeof asset2.label === "string") { - if (asset2.label.endsWith(targetTriple) || asset2.label.endsWith(targetDuple)) { + if (endsWithPlatform(asset2.label)) { return true; } } if (typeof asset2.name === "string") { - if (asset2.name.endsWith(targetTriple) || asset2.name.endsWith(targetDuple)) { + if (endsWithPlatform(asset2.name)) { return true; } } return false; }); if (matchingAssets.length === 0) { + const formats = [targetTriple, targetDuple]; + if (targetDupleUnderscore) { + formats.push(targetDupleUnderscore); + } throw new Error( - `Expected to find asset in release ${slug.owner}/${slug.repository}@${tag} with label or name ending in ${targetTriple} or ${targetDuple}` + `Expected to find asset in release ${slug.owner}/${slug.repository}@${tag} with label or name containing platform identifier ${formats.join(" or ")} at the end of the filename (before the extension)` ); } if (matchingAssets.length > 1) { + const formats = [targetTriple, targetDuple]; + if (targetDupleUnderscore) { + formats.push(targetDupleUnderscore); + } throw new Error( - `Ambiguous targets: expected to find a single asset in release ${slug.owner}/${slug.repository}@${tag} matching target triple ${targetTriple} or target duple ${targetDuple}, but found ${matchingAssets.length}. + `Ambiguous targets: expected to find a single asset in release ${slug.owner}/${slug.repository}@${tag} containing platform identifier ${formats.join(" or ")} at the end of the filename (before the extension), but found ${matchingAssets.length}. To resolve, specify the desired binary with the target format ${slug.owner}/${slug.repository}/@${tag}` ); @@ -10204,10 +10246,11 @@ To resolve, specify the desired binary with the target format ${slug.owner}/${sl const targetName = stripTargetTriple(matchField); return { binaryName: targetName, - url: asset.url + url: asset.url, + name: asset.name || "" }; } -async function fetchReleaseAssetMetadataFromTag(octokit, slug, binaryName, tag, targetTriple, targetDuple) { +async function fetchReleaseAssetMetadataFromTag(octokit, slug, binaryName, tag, targetTriple, targetDuple, targetDupleUnderscore) { const releaseMetadata = await octokit.rest.repos.getReleaseByTag({ owner: slug.owner, repo: slug.repository, @@ -10219,11 +10262,15 @@ async function fetchReleaseAssetMetadataFromTag(octokit, slug, binaryName, tag, binaryName, tag, targetTriple, - targetDuple + targetDuple, + targetDupleUnderscore ); } // src/index.ts +function isZipFile(filename) { + return filename.toLowerCase().endsWith(".zip"); +} function getDestinationDirectory(storageDirectory, slug, tag, platform2, architecture) { return path.join( storageDirectory, @@ -10238,6 +10285,7 @@ async function installGitHubReleaseBinary(octokit, targetRelease, storageDirecto const currentPlatform = (0, import_node_os.platform)(); const targetTriple = getTargetTriple(currentArch, currentPlatform); const targetDuple = getTargetDuple(currentArch, currentPlatform); + const targetDupleUnderscore = getTargetDupleUnderscore(currentArch, currentPlatform); const releaseTag = await findExactSemanticVersionTag( octokit, targetRelease.slug, @@ -10256,32 +10304,34 @@ async function installGitHubReleaseBinary(octokit, targetRelease, storageDirecto targetRelease.binaryName, releaseTag, targetTriple, - targetDuple + targetDuple, + targetDupleUnderscore ); const destinationBasename = unwrapOrDefault( releaseAsset.binaryName, targetRelease.slug.repository ); - const destinationFilename = path.join( - destinationDirectory, - destinationBasename - ); fs.mkdirSync(destinationDirectory, { recursive: true }); - if (fs.existsSync(destinationFilename)) { + const assetName = releaseAsset.name || ""; + core2.debug(`Asset name: ${assetName}`); + const isZip = isZipFile(assetName); + const destinationFilename = isZip ? path.join(destinationDirectory, `${destinationBasename}.zip`) : path.join(destinationDirectory, destinationBasename); + const finalBinaryPath = path.join(destinationDirectory, destinationBasename); + if (fs.existsSync(finalBinaryPath)) { if (ignoreExisting) { - core2.info(`Binary already exists at ${destinationFilename}, ignoring and leaving system as-is`); + core2.info(`Binary already exists at ${finalBinaryPath}, ignoring and leaving system as-is`); core2.addPath(destinationDirectory); return; } } - await tc.downloadTool( + const downloadedFilePath = await tc.downloadTool( releaseAsset.url, destinationFilename, `token ${token}`, { accept: "application/octet-stream" } ); if (isSome(targetRelease.checksum)) { - const fileBuffer = fs.readFileSync(destinationFilename); + const fileBuffer = fs.readFileSync(downloadedFilePath); const hash = (0, import_node_crypto.createHash)("sha256"); hash.update(fileBuffer); const calculatedChecksum = hash.digest("hex"); @@ -10298,7 +10348,34 @@ async function installGitHubReleaseBinary(octokit, targetRelease, storageDirecto ); } } - fs.chmodSync(destinationFilename, "755"); + if (isZip) { + core2.info(`Detected zip archive based on filename: ${assetName}`); + core2.info(`Extracting zip file: ${downloadedFilePath}`); + const extractedDirectory = await tc.extractZip(downloadedFilePath, destinationDirectory); + core2.debug(`Files extracted to ${extractedDirectory}`); + const extractedFiles = fs.readdirSync(extractedDirectory); + const visibleFiles = extractedFiles.filter( + (file) => !file.startsWith(".") && !fs.statSync(path.join(extractedDirectory, file)).isDirectory() + ); + if (visibleFiles.length !== 1) { + throw new Error(`Expected exactly one binary in the zip archive, but found ${visibleFiles.length} files: ${visibleFiles.join(", ")}`); + } + const binaryName = visibleFiles[0]; + core2.debug(`Found single binary in zip: ${binaryName}`); + fs.renameSync( + path.join(extractedDirectory, binaryName), + finalBinaryPath + ); + if (extractedDirectory !== destinationDirectory) { + core2.debug(`Removing temporary extraction directory: ${extractedDirectory}`); + fs.rmSync(extractedDirectory, { recursive: true, force: true }); + } + core2.debug(`Removing zip file: ${downloadedFilePath}`); + fs.unlinkSync(downloadedFilePath); + } else { + core2.debug(`Downloaded binary file: ${downloadedFilePath}`); + } + fs.chmodSync(finalBinaryPath, "755"); core2.addPath(destinationDirectory); } async function main() { diff --git a/src/fetch.ts b/src/fetch.ts index 74d66e6..f8b14a1 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -126,6 +126,7 @@ export async function findExactSemanticVersionTag( type ReleaseAssetMetadata = { binaryName: Option; url: string; + name?: string; }; /** @@ -152,23 +153,33 @@ export function findMatchingReleaseAssetMetadata( tag: ExactSemanticVersion, targetTriple: TargetTriple, targetDuple: TargetDuple, + targetDupleUnderscore?: TargetDuple, ): ReleaseAssetMetadata { // When the binary name is provided, look for matching binary with target triple or target duple if (isSome(binaryName)) { + // Standard hyphen format for target triple and duple const targetLabelTraditional = `${binaryName.value}-${targetTriple}`; const targetLabelDuple = `${binaryName.value}-${targetDuple}`; + // Underscore format for target duple (if provided) + const targetLabelDupleUnderscore = targetDupleUnderscore ? + `${binaryName.value}_${targetDupleUnderscore}` : ''; + const asset = releaseMetadata.data.assets.find((asset) => { // Check for label match if (typeof asset.label === "string") { - if (asset.label === targetLabelTraditional || asset.label === targetLabelDuple) { + if (asset.label === targetLabelTraditional || + asset.label === targetLabelDuple || + (targetLabelDupleUnderscore && asset.label === targetLabelDupleUnderscore)) { return true; } } // Check for name match if (typeof asset.name === "string") { - if (asset.name === targetLabelTraditional || asset.name === targetLabelDuple) { + if (asset.name === targetLabelTraditional || + asset.name === targetLabelDuple || + (targetLabelDupleUnderscore && asset.name === targetLabelDupleUnderscore)) { return true; } } @@ -177,14 +188,19 @@ export function findMatchingReleaseAssetMetadata( }); if (asset === undefined) { + const formats = [targetLabelTraditional, targetLabelDuple]; + if (targetDupleUnderscore) { + formats.push(targetLabelDupleUnderscore); + } throw new Error( - `Expected to find asset in release ${slug.owner}/${slug.repository}@${tag} with label or name ${targetLabelTraditional} or ${targetLabelDuple}`, + `Expected to find asset in release ${slug.owner}/${slug.repository}@${tag} with label or name ${formats.join(" or ")}`, ); } return { binaryName: binaryName, url: asset.url, + name: asset.name || '', }; } @@ -194,16 +210,29 @@ export function findMatchingReleaseAssetMetadata( // In both cases, we assume that's the binary the user meant. // If there is ambiguity, exit with an error. const matchingAssets = releaseMetadata.data.assets.filter((asset) => { + // Helper function to check if a name contains the platform identifier at the end + const endsWithPlatform = (name: string) => { + // Get just the filename part without the extension + const filenameWithoutExt = name.replace(/\.[^.]+$/, ''); + + return ( + // Traditional formats with platform at the end + filenameWithoutExt.endsWith(targetTriple) || + filenameWithoutExt.endsWith(targetDuple) || + (targetDupleUnderscore && filenameWithoutExt.endsWith(targetDupleUnderscore)) + ); + }; + // Check label match if (typeof asset.label === "string") { - if (asset.label.endsWith(targetTriple) || asset.label.endsWith(targetDuple)) { + if (endsWithPlatform(asset.label)) { return true; } } // Check name match if (typeof asset.name === "string") { - if (asset.name.endsWith(targetTriple) || asset.name.endsWith(targetDuple)) { + if (endsWithPlatform(asset.name)) { return true; } } @@ -211,13 +240,21 @@ export function findMatchingReleaseAssetMetadata( return false; }); if (matchingAssets.length === 0) { + const formats = [targetTriple, targetDuple]; + if (targetDupleUnderscore) { + formats.push(targetDupleUnderscore); + } throw new Error( - `Expected to find asset in release ${slug.owner}/${slug.repository}@${tag} with label or name ending in ${targetTriple} or ${targetDuple}`, + `Expected to find asset in release ${slug.owner}/${slug.repository}@${tag} with label or name containing platform identifier ${formats.join(" or ")} at the end of the filename (before the extension)`, ); } if (matchingAssets.length > 1) { + const formats = [targetTriple, targetDuple]; + if (targetDupleUnderscore) { + formats.push(targetDupleUnderscore); + } throw new Error( - `Ambiguous targets: expected to find a single asset in release ${slug.owner}/${slug.repository}@${tag} matching target triple ${targetTriple} or target duple ${targetDuple}, but found ${matchingAssets.length}. + `Ambiguous targets: expected to find a single asset in release ${slug.owner}/${slug.repository}@${tag} containing platform identifier ${formats.join(" or ")} at the end of the filename (before the extension), but found ${matchingAssets.length}. To resolve, specify the desired binary with the target format ${slug.owner}/${slug.repository}/@${tag}`, ); @@ -237,6 +274,7 @@ To resolve, specify the desired binary with the target format ${slug.owner}/${sl return { binaryName: targetName, url: asset.url, + name: asset.name || '', }; } @@ -247,6 +285,7 @@ export async function fetchReleaseAssetMetadataFromTag( tag: ExactSemanticVersion, targetTriple: TargetTriple, targetDuple: TargetDuple, + targetDupleUnderscore?: TargetDuple, ): Promise { // Maintainer's note: this impure function call makes this function difficult to test. const releaseMetadata = await octokit.rest.repos.getReleaseByTag({ @@ -262,5 +301,6 @@ export async function fetchReleaseAssetMetadataFromTag( tag, targetTriple, targetDuple, + targetDupleUnderscore, ); } diff --git a/src/index.ts b/src/index.ts index c04f10e..9ac2882 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ import { parseTargetReleases, parseToken, } from "./parse"; -import { getTargetTriple, getTargetDuple } from "./platform"; +import { getTargetTriple, getTargetDuple, getTargetDupleUnderscore } from "./platform"; import { fetchReleaseAssetMetadataFromTag, findExactSemanticVersionTag, @@ -25,6 +25,13 @@ import type { } from "./types"; import { isSome, unwrapOrDefault } from "./option"; +/** + * Check if a file is a zip archive based on its extension + */ +function isZipFile(filename: string): boolean { + return filename.toLowerCase().endsWith('.zip'); +} + function getDestinationDirectory( storageDirectory: string, slug: RepositorySlug, @@ -52,6 +59,7 @@ async function installGitHubReleaseBinary( const currentPlatform = platform(); const targetTriple = getTargetTriple(currentArch, currentPlatform); const targetDuple = getTargetDuple(currentArch, currentPlatform); + const targetDupleUnderscore = getTargetDupleUnderscore(currentArch, currentPlatform); const releaseTag = await findExactSemanticVersionTag( octokit, @@ -74,39 +82,52 @@ async function installGitHubReleaseBinary( releaseTag, targetTriple, targetDuple, + targetDupleUnderscore, ); const destinationBasename = unwrapOrDefault( releaseAsset.binaryName, targetRelease.slug.repository, ); - const destinationFilename = path.join( - destinationDirectory, - destinationBasename, - ); + // Create the destination directory fs.mkdirSync(destinationDirectory, { recursive: true }); + // Determine if we're dealing with a zip file based on the asset name + const assetName = releaseAsset.name || ''; + core.debug(`Asset name: ${assetName}`); + const isZip = isZipFile(assetName); + + // If it's a standard binary, use the existing destination path + // If it's a zip, we'll create a temporary file path for the download + const destinationFilename = isZip + ? path.join(destinationDirectory, `${destinationBasename}.zip`) + : path.join(destinationDirectory, destinationBasename); + + // Final binary path to check/add to PATH (same for non-zip, different for zip) + const finalBinaryPath = path.join(destinationDirectory, destinationBasename); + // Check if file already exists and skip if ignoreExisting is true - if (fs.existsSync(destinationFilename)) { + if (fs.existsSync(finalBinaryPath)) { if (ignoreExisting) { - core.info(`Binary already exists at ${destinationFilename}, ignoring and leaving system as-is`); + core.info(`Binary already exists at ${finalBinaryPath}, ignoring and leaving system as-is`); // Still add the directory to PATH so the binary can be found core.addPath(destinationDirectory); return; } } - await tc.downloadTool( + // Download the file + const downloadedFilePath = await tc.downloadTool( releaseAsset.url, destinationFilename, `token ${token}`, { accept: "application/octet-stream" }, ); - // Ensure the binary matches the expected checksum + // Ensure the downloaded file matches the expected checksum if (isSome(targetRelease.checksum)) { - const fileBuffer = fs.readFileSync(destinationFilename); + const fileBuffer = fs.readFileSync(downloadedFilePath); const hash = createHash("sha256"); hash.update(fileBuffer); const calculatedChecksum = hash.digest("hex"); @@ -124,9 +145,56 @@ async function installGitHubReleaseBinary( } } + // Process the file based on type + if (isZip) { + core.info(`Detected zip archive based on filename: ${assetName}`); + core.info(`Extracting zip file: ${downloadedFilePath}`); + + // Extract the zip file + const extractedDirectory = await tc.extractZip(downloadedFilePath, destinationDirectory); + core.debug(`Files extracted to ${extractedDirectory}`); + + // We assume there's exactly one binary file in the archive + // If there's more than one file or if it's a directory, throw an error + const extractedFiles = fs.readdirSync(extractedDirectory); + + // Filter out hidden files and directories + const visibleFiles = extractedFiles.filter(file => + !file.startsWith('.') && !fs.statSync(path.join(extractedDirectory, file)).isDirectory() + ); + + if (visibleFiles.length !== 1) { + throw new Error(`Expected exactly one binary in the zip archive, but found ${visibleFiles.length} files: ${visibleFiles.join(', ')}`); + } + + // Use the single binary file - we've already checked that there's exactly one file + // TypeScript needs a non-null assertion here to know it's safe + const binaryName: string = visibleFiles[0]!; + core.debug(`Found single binary in zip: ${binaryName}`); + + // Move the binary to the destination + fs.renameSync( + path.join(extractedDirectory, binaryName), + finalBinaryPath + ); + + // Clean up the extracted directory + if (extractedDirectory !== destinationDirectory) { + core.debug(`Removing temporary extraction directory: ${extractedDirectory}`); + fs.rmSync(extractedDirectory, { recursive: true, force: true }); + } + + // Clean up the zip file + core.debug(`Removing zip file: ${downloadedFilePath}`); + fs.unlinkSync(downloadedFilePath); + } else { + // For regular binaries, the downloaded file is already at the right location + core.debug(`Downloaded binary file: ${downloadedFilePath}`); + } + // Permissions are an attribute of the filesystem, not the file. // Set the executable permission on the binary no matter where it came from. - fs.chmodSync(destinationFilename, "755"); + fs.chmodSync(finalBinaryPath, "755"); core.addPath(destinationDirectory); } diff --git a/src/platform.ts b/src/platform.ts index fed675e..5628f26 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -9,11 +9,18 @@ const ALL_TARGET_TRIPLES: readonly TargetTriple[] = [ ] as unknown as readonly TargetTriple[]; // Target duples (Go format OS-architecture combinations) +// Both hyphenated and underscore versions are included for compatibility export const TARGET_DUPLES: readonly TargetDuple[] = [ + // Hyphenated versions "linux-amd64", "linux-arm64", "darwin-amd64", "darwin-arm64", + // Underscore versions + "linux_amd64", + "linux_arm64", + "darwin_amd64", + "darwin_arm64", ] as unknown as readonly TargetDuple[]; function architectureLabel(arch: string): string { @@ -62,12 +69,13 @@ export function getTargetTriple( } /** - * Get the target duple (e.g. "linux-amd64") for the given architecture and platform + * Get the target duples (e.g. "linux-amd64" and "linux_amd64") for the given architecture and platform */ export function getTargetDuple( arch: string, platform: NodeJS.Platform, ): TargetDuple { + // Return the hyphenated version as primary switch (platform) { case "darwin": return arch === "arm64" ? "darwin-arm64" as TargetDuple : "darwin-amd64" as TargetDuple; @@ -80,6 +88,26 @@ export function getTargetDuple( } } +/** + * Get the underscore version of target duple (e.g. "linux_amd64") for the given architecture and platform + */ +export function getTargetDupleUnderscore( + arch: string, + platform: NodeJS.Platform, +): TargetDuple { + // Return the underscore version + switch (platform) { + case "darwin": + return arch === "arm64" ? "darwin_arm64" as TargetDuple : "darwin_amd64" as TargetDuple; + case "linux": + return arch === "arm64" ? "linux_arm64" as TargetDuple : "linux_amd64" as TargetDuple; + default: + throw new Error( + `Unsupported platform ${platform} for target duple conversion`, + ); + } +} + /** * Strip the target triple or target duple suffix from a string */ @@ -105,9 +133,13 @@ export function stripTargetTriple(value: string): Option { return some(strippedTriple); } - // Try to strip target duple suffix + // Try to strip target duple suffix (both hyphenated and underscore versions) const strippedDuple = TARGET_DUPLES.reduce( - (value, duple) => value.replace(new RegExp(`-${duple}$`), ""), + (value, duple) => { + // Replace suffix if it matches directly (handles both - and _) + const pattern = new RegExp(`[-_]${duple}$`); + return value.replace(pattern, ""); + }, value, ); diff --git a/test/test-find-matching-release-asset.ts b/test/test-find-matching-release-asset.ts index e669ab2..3eed70b 100644 --- a/test/test-find-matching-release-asset.ts +++ b/test/test-find-matching-release-asset.ts @@ -47,7 +47,8 @@ test("should find asset with binary name using target triple", () => { assert.deepEqual(result, { binaryName, - url: "https://example.com/testbin-triple" + url: "https://example.com/testbin-triple", + name: '' }); }); @@ -69,7 +70,8 @@ test("should find asset with binary name using target duple", () => { assert.deepEqual(result, { binaryName, - url: "https://example.com/testbin-duple" + url: "https://example.com/testbin-duple", + name: '' }); }); @@ -109,7 +111,8 @@ test("should find asset without binary name using target triple", () => { assert.deepEqual(result, { binaryName: some("somebin"), - url: "https://example.com/somebin-triple" + url: "https://example.com/somebin-triple", + name: '' }); }); @@ -129,7 +132,8 @@ test("should find asset without binary name using target duple", () => { assert.deepEqual(result, { binaryName: some("somebin"), - url: "https://example.com/somebin-duple" + url: "https://example.com/somebin-duple", + name: '' }); }); @@ -204,7 +208,8 @@ test("should find x86_64 asset with binary name using target triple", () => { assert.deepEqual(result, { binaryName, - url: "https://example.com/testbin-x86-triple" + url: "https://example.com/testbin-x86-triple", + name: '' }); }); @@ -225,7 +230,8 @@ test("should find x86_64 asset with binary name using target duple", () => { assert.deepEqual(result, { binaryName, - url: "https://example.com/testbin-x86-duple" + url: "https://example.com/testbin-x86-duple", + name: '' }); }); @@ -248,7 +254,8 @@ test("should find asset with binary name using name property with target triple" assert.deepEqual(result, { binaryName, - url: "https://example.com/testbin-triple" + url: "https://example.com/testbin-triple", + name: 'testbin-aarch64-apple-darwin' }); }); @@ -270,7 +277,8 @@ test("should find asset with binary name using name property with target duple", assert.deepEqual(result, { binaryName, - url: "https://example.com/testbin-duple" + url: "https://example.com/testbin-duple", + name: 'testbin-darwin-arm64' }); }); @@ -291,7 +299,8 @@ test("should find asset without binary name using name property with target trip assert.deepEqual(result, { binaryName: some("somebin"), - url: "https://example.com/somebin-triple" + url: "https://example.com/somebin-triple", + name: 'somebin-aarch64-apple-darwin' }); }); @@ -312,6 +321,7 @@ test("should find asset without binary name using name property with target dupl assert.deepEqual(result, { binaryName: some("somebin"), - url: "https://example.com/somebin-duple" + url: "https://example.com/somebin-duple", + name: 'somebin-darwin-arm64' }); }); \ No newline at end of file