Skip to content

Commit f355ab9

Browse files
committed
feat: support unpacking binary from zip file
TICKET: VL-4163
1 parent ac5cec8 commit f355ab9

File tree

6 files changed

+157
-35
lines changed

6 files changed

+157
-35
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ The primary goals are, in order of priority:
1717
This action only supports installing from releases where the release:
1818

1919
- is tagged with the full `{major}.{minor}.{patch}` semantic version
20-
- contains raw binary assets (archives not supported)
20+
- contains raw binary assets or zip archives (zip archives must contain exactly one binary file)
2121
- assets are labeled with the binary name and [target triple] in the format `<binary name>-<target triple>`
2222

2323
You can create compatible releases with [semantic-release], using a workflow like [semantic-release-action/rust].
@@ -63,6 +63,16 @@ Install a binary from a release with multiple binaries available:
6363
EricCrosson/future-tools/flux-capacitor@v1
6464
```
6565

66+
Install a binary from a zip archive (the zip must contain exactly one binary file):
67+
68+
```yaml
69+
- name: Install FOSSA CLI
70+
uses: EricCrosson/install-github-release-binary@v2
71+
with:
72+
targets: |
73+
fossas/fossa-cli@v3.11.7:sha256-d6f73d3da1cc7727610dd3f2c1a6021aeb23516f74b6f031e91deb31eba34f2b
74+
```
75+
6676
## Inputs
6777

6878
| Input Parameter | Required | Description |

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: Install GitHub Release binary
3-
description: Install a binary from a GitHub Release
3+
description: Install a binary from a GitHub Release (supports both direct binaries and zip archives)
44
author: Eric Crosson
55
branding:
66
icon: arrow-down

dist/index.js

Lines changed: 46 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/fetch.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export async function findExactSemanticVersionTag(
126126
type ReleaseAssetMetadata = {
127127
binaryName: Option<string>;
128128
url: string;
129+
name?: string;
129130
};
130131

131132
/**
@@ -185,6 +186,7 @@ export function findMatchingReleaseAssetMetadata(
185186
return {
186187
binaryName: binaryName,
187188
url: asset.url,
189+
name: asset.name || '',
188190
};
189191
}
190192

@@ -237,6 +239,7 @@ To resolve, specify the desired binary with the target format ${slug.owner}/${sl
237239
return {
238240
binaryName: targetName,
239241
url: asset.url,
242+
name: asset.name || '',
240243
};
241244
}
242245

src/index.ts

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ import type {
2525
} from "./types";
2626
import { isSome, unwrapOrDefault } from "./option";
2727

28+
/**
29+
* Check if a file is a zip archive based on its extension
30+
*/
31+
function isZipFile(filename: string): boolean {
32+
return filename.toLowerCase().endsWith('.zip');
33+
}
34+
2835
function getDestinationDirectory(
2936
storageDirectory: string,
3037
slug: RepositorySlug,
@@ -80,33 +87,45 @@ async function installGitHubReleaseBinary(
8087
releaseAsset.binaryName,
8188
targetRelease.slug.repository,
8289
);
83-
const destinationFilename = path.join(
84-
destinationDirectory,
85-
destinationBasename,
86-
);
8790

91+
// Create the destination directory
8892
fs.mkdirSync(destinationDirectory, { recursive: true });
8993

94+
// Determine if we're dealing with a zip file based on the asset name
95+
const assetName = releaseAsset.name || '';
96+
core.debug(`Asset name: ${assetName}`);
97+
const isZip = isZipFile(assetName);
98+
99+
// If it's a standard binary, use the existing destination path
100+
// If it's a zip, we'll create a temporary file path for the download
101+
const destinationFilename = isZip
102+
? path.join(destinationDirectory, `${destinationBasename}.zip`)
103+
: path.join(destinationDirectory, destinationBasename);
104+
105+
// Final binary path to check/add to PATH (same for non-zip, different for zip)
106+
const finalBinaryPath = path.join(destinationDirectory, destinationBasename);
107+
90108
// Check if file already exists and skip if ignoreExisting is true
91-
if (fs.existsSync(destinationFilename)) {
109+
if (fs.existsSync(finalBinaryPath)) {
92110
if (ignoreExisting) {
93-
core.info(`Binary already exists at ${destinationFilename}, ignoring and leaving system as-is`);
111+
core.info(`Binary already exists at ${finalBinaryPath}, ignoring and leaving system as-is`);
94112
// Still add the directory to PATH so the binary can be found
95113
core.addPath(destinationDirectory);
96114
return;
97115
}
98116
}
99117

100-
await tc.downloadTool(
118+
// Download the file
119+
const downloadedFilePath = await tc.downloadTool(
101120
releaseAsset.url,
102121
destinationFilename,
103122
`token ${token}`,
104123
{ accept: "application/octet-stream" },
105124
);
106125

107-
// Ensure the binary matches the expected checksum
126+
// Ensure the downloaded file matches the expected checksum
108127
if (isSome(targetRelease.checksum)) {
109-
const fileBuffer = fs.readFileSync(destinationFilename);
128+
const fileBuffer = fs.readFileSync(downloadedFilePath);
110129
const hash = createHash("sha256");
111130
hash.update(fileBuffer);
112131
const calculatedChecksum = hash.digest("hex");
@@ -124,9 +143,56 @@ async function installGitHubReleaseBinary(
124143
}
125144
}
126145

146+
// Process the file based on type
147+
if (isZip) {
148+
core.info(`Detected zip archive based on filename: ${assetName}`);
149+
core.info(`Extracting zip file: ${downloadedFilePath}`);
150+
151+
// Extract the zip file
152+
const extractedDirectory = await tc.extractZip(downloadedFilePath, destinationDirectory);
153+
core.debug(`Files extracted to ${extractedDirectory}`);
154+
155+
// We assume there's exactly one binary file in the archive
156+
// If there's more than one file or if it's a directory, throw an error
157+
const extractedFiles = fs.readdirSync(extractedDirectory);
158+
159+
// Filter out hidden files and directories
160+
const visibleFiles = extractedFiles.filter(file =>
161+
!file.startsWith('.') && !fs.statSync(path.join(extractedDirectory, file)).isDirectory()
162+
);
163+
164+
if (visibleFiles.length !== 1) {
165+
throw new Error(`Expected exactly one binary in the zip archive, but found ${visibleFiles.length} files: ${visibleFiles.join(', ')}`);
166+
}
167+
168+
// Use the single binary file - we've already checked that there's exactly one file
169+
// TypeScript needs a non-null assertion here to know it's safe
170+
const binaryName: string = visibleFiles[0]!;
171+
core.debug(`Found single binary in zip: ${binaryName}`);
172+
173+
// Move the binary to the destination
174+
fs.renameSync(
175+
path.join(extractedDirectory, binaryName),
176+
finalBinaryPath
177+
);
178+
179+
// Clean up the extracted directory
180+
if (extractedDirectory !== destinationDirectory) {
181+
core.debug(`Removing temporary extraction directory: ${extractedDirectory}`);
182+
fs.rmSync(extractedDirectory, { recursive: true, force: true });
183+
}
184+
185+
// Clean up the zip file
186+
core.debug(`Removing zip file: ${downloadedFilePath}`);
187+
fs.unlinkSync(downloadedFilePath);
188+
} else {
189+
// For regular binaries, the downloaded file is already at the right location
190+
core.debug(`Downloaded binary file: ${downloadedFilePath}`);
191+
}
192+
127193
// Permissions are an attribute of the filesystem, not the file.
128194
// Set the executable permission on the binary no matter where it came from.
129-
fs.chmodSync(destinationFilename, "755");
195+
fs.chmodSync(finalBinaryPath, "755");
130196
core.addPath(destinationDirectory);
131197
}
132198

0 commit comments

Comments
 (0)