fix: use sh -c in Docker so sitespeed.io resolves via shell PATH
execSync/which inside Node.js doesn't see the full container PATH.
Switching to spawn('sh', ['-c', shellCmd]) so the shell resolves
sitespeed.io exactly as the original image entrypoint does.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
70
runner.js
70
runner.js
@@ -1,44 +1,20 @@
|
||||
import { spawn, execSync } from 'child_process';
|
||||
import { spawn } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const LOCAL_BIN = join(__dirname, '..', 'sitespeed.io', 'bin', 'sitespeed.js');
|
||||
|
||||
// Resolve sitespeed.io binary.
|
||||
// In Docker the global install is on PATH; outside Docker use the local clone.
|
||||
function resolveBin() {
|
||||
if (process.env.SITESPEED_BIN) return { cmd: 'node', scriptArg: process.env.SITESPEED_BIN };
|
||||
|
||||
// Try global install on PATH (works in Docker image)
|
||||
try {
|
||||
const globalPath = execSync('which sitespeed.io', { encoding: 'utf8' }).trim();
|
||||
if (globalPath) return { cmd: globalPath, scriptArg: null };
|
||||
} catch {}
|
||||
|
||||
// Try common global npm location
|
||||
const candidates = [
|
||||
'/usr/local/lib/node_modules/sitespeed.io/bin/sitespeed.js',
|
||||
'/usr/lib/node_modules/sitespeed.io/bin/sitespeed.js',
|
||||
join(__dirname, '..', 'sitespeed.io', 'bin', 'sitespeed.js'),
|
||||
];
|
||||
for (const p of candidates) {
|
||||
if (existsSync(p)) return { cmd: 'node', scriptArg: p };
|
||||
}
|
||||
|
||||
throw new Error('sitespeed.io binary not found. Set SITESPEED_BIN env var.');
|
||||
// Shell-escape a single argument (single-quote wrapping)
|
||||
function q(arg) {
|
||||
return `'${String(arg).replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
export function runTest(job, onLine) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const outputFolder = join(__dirname, 'reports', job.id);
|
||||
|
||||
let bin, scriptArg;
|
||||
try {
|
||||
({ cmd: bin, scriptArg } = resolveBin());
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const isDocker = !!process.env.IN_DOCKER;
|
||||
|
||||
const sitespeedArgs = [
|
||||
job.url,
|
||||
@@ -53,27 +29,34 @@ export function runTest(job, onLine) {
|
||||
|
||||
if (job.mobile) sitespeedArgs.push('--mobile');
|
||||
|
||||
// Chrome flags required in Docker
|
||||
if (process.env.IN_DOCKER) {
|
||||
if (isDocker) {
|
||||
sitespeedArgs.push('--browsertime.chrome.args', 'no-sandbox');
|
||||
sitespeedArgs.push('--browsertime.chrome.args', 'disable-dev-shm-usage');
|
||||
sitespeedArgs.push('--browsertime.chrome.args', 'disable-gpu');
|
||||
}
|
||||
|
||||
// Build the final argv for spawn
|
||||
const spawnArgs = scriptArg
|
||||
? [scriptArg, ...sitespeedArgs] // node /path/to/sitespeed.js <args>
|
||||
: sitespeedArgs; // sitespeed.io <args>
|
||||
|
||||
const env = { ...process.env, DISPLAY: process.env.DISPLAY || ':99' };
|
||||
|
||||
onLine(`[runner] binary: ${bin}${scriptArg ? ' ' + scriptArg : ''}`);
|
||||
onLine(`[runner] DISPLAY: ${env.DISPLAY}`);
|
||||
onLine(`[runner] outputFolder: ${outputFolder}`);
|
||||
let child;
|
||||
|
||||
const child = spawn(bin, spawnArgs, { cwd: __dirname, env });
|
||||
if (isDocker) {
|
||||
// In Docker, sitespeed.io is on PATH but execSync can't see it from Node's
|
||||
// limited environment. Use 'sh -c' so the full shell PATH is used.
|
||||
const shellCmd = ['sitespeed.io', ...sitespeedArgs].map(q).join(' ');
|
||||
onLine(`[runner] sh -c ${shellCmd.slice(0, 120)}...`);
|
||||
onLine(`[runner] DISPLAY=${env.DISPLAY}`);
|
||||
child = spawn('sh', ['-c', shellCmd], { cwd: __dirname, env });
|
||||
} else {
|
||||
if (!existsSync(LOCAL_BIN)) {
|
||||
return reject(new Error(
|
||||
`Local sitespeed.io not found at ${LOCAL_BIN}\n` +
|
||||
`Run: cd /home/malin/c0ding/sitespeed.io && npm install`
|
||||
));
|
||||
}
|
||||
onLine(`[runner] node ${LOCAL_BIN.slice(-40)}...`);
|
||||
child = spawn('node', [LOCAL_BIN, ...sitespeedArgs], { cwd: __dirname, env });
|
||||
}
|
||||
|
||||
// Collect all output lines — included in error if process fails
|
||||
const allLines = [];
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
@@ -90,14 +73,13 @@ export function runTest(job, onLine) {
|
||||
if (code === 0) {
|
||||
resolve(outputFolder);
|
||||
} else {
|
||||
// Surface the last 20 lines of output so the error is visible in the UI
|
||||
const tail = allLines.slice(-20).join('\n');
|
||||
reject(new Error(`sitespeed.io exited with code ${code}\n${tail}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(new Error(`Failed to spawn sitespeed.io (${bin}): ${err.message}`));
|
||||
reject(new Error(`Failed to spawn process: ${err.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user