Skip to content

Commit aadff87

Browse files
committed
fix: Improve npm package implementation and CI testing
Package improvements: - Add Windows binary support (.exe extension) in getBinary.js and run.js - Add retry logic with exponential backoff for download failures - Add security filters for tarball extraction (only allow binary files) - Add binary existence check to skip redundant downloads - Add better error handling in run.js with helpful reinstall instructions - Support --local-tarball CLI argument for testing without network calls CI workflow improvements: - Fix pnpm test failure by directly executing binary instead of using pnpm exec - Test actual production code using --local-tarball argument - Remove code generation in CI workflow (security improvement) - Simplify workflow by removing duplicate heredoc code - Use --ignore-scripts and manual script execution for better control Addresses Copilot review comments: - Windows .exe support (comments #2452017892, #2452017916) - Error handling in run.js (comment #2452017785) - Reduced CI workflow complexity
1 parent 30b8b5c commit aadff87

File tree

4 files changed

+124
-130
lines changed

4 files changed

+124
-130
lines changed

.github/npm/getBinary.js

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import fetch from 'node-fetch';
2-
import { createWriteStream, mkdirSync, chmodSync, existsSync, readFileSync } from 'fs';
2+
import { createReadStream, createWriteStream, mkdirSync, chmodSync, existsSync, readFileSync } from 'fs';
33
import { fileURLToPath } from 'url';
44
import { dirname, join } from 'path';
55
import { pipeline } from 'stream/promises';
@@ -41,43 +41,104 @@ function getVersion() {
4141
return packageJson.version;
4242
}
4343

44-
async function downloadBinary() {
44+
async function downloadBinary(options = {}) {
4545
const platform = getPlatform();
4646
const version = getVersion();
47-
const url = `https://github.com/coralogix/protofetch/releases/download/v${version}/protofetch_${platform}.tar.gz`;
47+
48+
// Support local tarball for testing (not exposed via postinstall, only via direct script call)
49+
const localTarball = options.localTarball;
50+
const url = localTarball
51+
? null
52+
: `https://github.com/coralogix/protofetch/releases/download/v${version}/protofetch_${platform}.tar.gz`;
4853

4954
const binDir = join(__dirname, 'bin');
55+
const isWindows = process.platform === 'win32';
56+
const binaryName = isWindows ? 'protofetch.exe' : 'protofetch';
57+
const binaryPath = join(binDir, binaryName);
58+
59+
// Skip download if binary already exists
60+
if (existsSync(binaryPath)) {
61+
console.log('protofetch binary already installed, skipping download');
62+
return;
63+
}
5064

5165
// Create bin directory if it doesn't exist
5266
if (!existsSync(binDir)) {
5367
mkdirSync(binDir, { recursive: true });
5468
}
5569

56-
console.log(`Downloading protofetch binary from ${url}...`);
57-
58-
const response = await fetch(url);
59-
60-
if (!response.ok) {
61-
throw new Error(`Failed to download binary: ${response.status} ${response.statusText}`);
70+
if (localTarball) {
71+
console.log(`Installing protofetch binary from local tarball: ${localTarball}...`);
72+
} else {
73+
console.log(`Downloading protofetch binary from ${url}...`);
6274
}
6375

64-
// Extract tarball directly to bin directory
65-
await pipeline(
66-
response.body,
67-
tar.extract({
68-
cwd: binDir,
69-
strip: 1 // Strip the 'bin/' prefix from tarball paths
70-
})
71-
);
72-
73-
// Make the binary executable (Unix systems)
74-
const binaryPath = join(binDir, 'protofetch');
75-
if (existsSync(binaryPath)) {
76-
chmodSync(binaryPath, 0o755);
77-
console.log('protofetch binary installed successfully');
78-
} else {
79-
throw new Error('Binary extraction failed - protofetch binary not found');
76+
// Retry logic for transient failures
77+
let lastError;
78+
const maxAttempts = localTarball ? 1 : 3;
79+
80+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
81+
try {
82+
let sourceStream;
83+
84+
if (localTarball) {
85+
// Use local tarball for testing
86+
const absolutePath = join(__dirname, localTarball);
87+
sourceStream = createReadStream(absolutePath);
88+
} else {
89+
// Download from GitHub releases
90+
const response = await fetch(url, {
91+
redirect: 'follow',
92+
timeout: 60000 // 60 second timeout
93+
});
94+
95+
if (!response.ok) {
96+
throw new Error(`Failed to download binary (HTTP ${response.status}): ${response.statusText}`);
97+
}
98+
99+
sourceStream = response.body;
100+
}
101+
102+
// Extract tarball with security filters
103+
await pipeline(
104+
sourceStream,
105+
tar.extract({
106+
cwd: binDir,
107+
strip: 1, // Strip the 'bin/' prefix from tarball paths
108+
strict: true,
109+
preservePaths: false,
110+
preserveOwner: false,
111+
filter: (path, entry) => {
112+
// Only allow the protofetch binary, reject symlinks and other files
113+
const allowedFiles = ['protofetch', 'protofetch.exe'];
114+
const fileName = path.split('/').pop();
115+
return entry.type === 'File' && allowedFiles.includes(fileName);
116+
}
117+
})
118+
);
119+
120+
// Make the binary executable (Unix systems only)
121+
if (!isWindows && existsSync(binaryPath)) {
122+
chmodSync(binaryPath, 0o755);
123+
}
124+
125+
if (existsSync(binaryPath)) {
126+
console.log('protofetch binary installed successfully');
127+
return;
128+
} else {
129+
throw new Error(`Binary extraction failed - ${binaryName} not found after extraction`);
130+
}
131+
} catch (error) {
132+
lastError = error;
133+
if (attempt < 3) {
134+
console.log(`Download attempt ${attempt} failed, retrying...`);
135+
// Exponential backoff: 1s, 2s
136+
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
137+
}
138+
}
80139
}
140+
141+
throw new Error(`Failed to download protofetch after 3 attempts: ${lastError.message}`);
81142
}
82143

83144
export { downloadBinary };

.github/npm/run.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,21 @@ import { fileURLToPath } from 'url';
66
const __filename = fileURLToPath(import.meta.url);
77
const __dirname = path.dirname(__filename);
88

9-
const binaryPath = path.join(__dirname, 'bin', 'protofetch');
9+
const isWindows = process.platform === 'win32';
10+
const binaryName = isWindows ? 'protofetch.exe' : 'protofetch';
11+
const binaryPath = path.join(__dirname, 'bin', binaryName);
12+
1013
const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
1114

15+
child.on('error', (error) => {
16+
console.error(`Failed to start protofetch: ${error.message}`);
17+
console.error('The binary may be missing or corrupted. Try reinstalling the package:');
18+
console.error(' npm install --force');
19+
console.error(' or');
20+
console.error(' pnpm install --force');
21+
process.exit(1);
22+
});
23+
1224
child.on('close', (code) => {
1325
process.exit(code);
1426
});

.github/npm/scripts.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { downloadBinary } from './getBinary.js';
22

33
if (process.argv.includes('install')) {
4-
downloadBinary()
4+
// Check for --local-tarball argument for testing
5+
const localTarballArg = process.argv.find(arg => arg.startsWith('--local-tarball='));
6+
const localTarball = localTarballArg ? localTarballArg.split('=')[1] : null;
7+
8+
downloadBinary({ localTarball })
59
.then(() => {
610
console.log('Installation complete');
711
process.exit(0);

.github/workflows/ci.yml

Lines changed: 20 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -209,60 +209,18 @@ jobs:
209209
run: |
210210
cd .github/npm
211211
212-
# Create test version of getBinary.js that uses local tarball
213-
cat > getBinary.test.js << 'EOF'
214-
import { createReadStream, mkdirSync, chmodSync, existsSync } from 'fs';
215-
import { fileURLToPath } from 'url';
216-
import { dirname, join } from 'path';
217-
import { pipeline } from 'stream/promises';
218-
import * as tar from 'tar';
212+
# Install dependencies
213+
npm install --ignore-scripts
219214
220-
const __filename = fileURLToPath(import.meta.url);
221-
const __dirname = dirname(__filename);
215+
# Run installation script with local tarball
216+
node scripts.js install --local-tarball=test-release/protofetch_${{ matrix.os.platform }}.tar.gz
222217
223-
async function downloadBinary() {
224-
const binDir = join(__dirname, 'bin');
225-
226-
if (!existsSync(binDir)) {
227-
mkdirSync(binDir, { recursive: true });
228-
}
229-
230-
console.log('Installing protofetch binary from local tarball...');
231-
232-
const tarballPath = join(__dirname, 'test-release', 'protofetch_${{ matrix.os.platform }}.tar.gz');
233-
234-
await pipeline(
235-
createReadStream(tarballPath),
236-
tar.extract({
237-
cwd: binDir,
238-
strip: 1
239-
})
240-
);
241-
242-
const binaryPath = join(binDir, 'protofetch');
243-
if (existsSync(binaryPath)) {
244-
chmodSync(binaryPath, 0o755);
245-
console.log('protofetch binary installed successfully');
246-
} else {
247-
throw new Error('Binary extraction failed - protofetch binary not found');
248-
}
249-
}
250-
251-
export { downloadBinary };
252-
EOF
253-
254-
# Backup original and use test version
255-
mv getBinary.js getBinary.original.js
256-
mv getBinary.test.js getBinary.js
257-
258-
# Install with npm
259-
npm install
260-
261-
# Verify binary works
262-
npm exec protofetch -- --version
263-
264-
# Restore original
265-
mv getBinary.original.js getBinary.js
218+
# Verify binary was extracted and works
219+
if [ "${{ runner.os }}" = "Windows" ]; then
220+
./bin/protofetch.exe --version
221+
else
222+
./bin/protofetch --version
223+
fi
266224
267225
# Clean up
268226
rm -rf node_modules package-lock.json bin/
@@ -272,59 +230,18 @@ jobs:
272230
run: |
273231
cd .github/npm
274232
275-
# Create test version of getBinary.js
276-
cat > getBinary.test.js << 'EOF'
277-
import { createReadStream, mkdirSync, chmodSync, existsSync } from 'fs';
278-
import { fileURLToPath } from 'url';
279-
import { dirname, join } from 'path';
280-
import { pipeline } from 'stream/promises';
281-
import * as tar from 'tar';
282-
283-
const __filename = fileURLToPath(import.meta.url);
284-
const __dirname = dirname(__filename);
285-
286-
async function downloadBinary() {
287-
const binDir = join(__dirname, 'bin');
288-
289-
if (!existsSync(binDir)) {
290-
mkdirSync(binDir, { recursive: true });
291-
}
292-
293-
console.log('Installing protofetch binary from local tarball...');
294-
295-
const tarballPath = join(__dirname, 'test-release', 'protofetch_${{ matrix.os.platform }}.tar.gz');
296-
297-
await pipeline(
298-
createReadStream(tarballPath),
299-
tar.extract({
300-
cwd: binDir,
301-
strip: 1
302-
})
303-
);
304-
305-
const binaryPath = join(binDir, 'protofetch');
306-
if (existsSync(binaryPath)) {
307-
chmodSync(binaryPath, 0o755);
308-
console.log('protofetch binary installed successfully');
309-
} else {
310-
throw new Error('Binary extraction failed - protofetch binary not found');
311-
}
312-
}
313-
314-
export { downloadBinary };
315-
EOF
316-
317-
mv getBinary.js getBinary.original.js
318-
mv getBinary.test.js getBinary.js
319-
320-
# Install with pnpm
321-
pnpm install
233+
# Install dependencies
234+
pnpm install --ignore-scripts
322235
323-
# Verify binary works
324-
pnpm exec protofetch -- --version
236+
# Run installation script with local tarball
237+
node scripts.js install --local-tarball=test-release/protofetch_${{ matrix.os.platform }}.tar.gz
325238
326-
# Restore original
327-
mv getBinary.original.js getBinary.js
239+
# Verify binary was extracted and works
240+
if [ "${{ runner.os }}" = "Windows" ]; then
241+
./bin/protofetch.exe --version
242+
else
243+
./bin/protofetch --version
244+
fi
328245
329246
release:
330247
runs-on: ubuntu-latest

0 commit comments

Comments
 (0)