Skip to content

Commit 3b0416e

Browse files
committed
feat(satellite): install TypeScript globally for deployment builds
1 parent c5a8ada commit 3b0416e

File tree

2 files changed

+207
-114
lines changed

2 files changed

+207
-114
lines changed

services/satellite/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ RUN pip3 install --break-system-packages uv && \
2424
which uvx && \
2525
uv --version
2626

27+
# Install TypeScript globally for GitHub deployment builds
28+
# Available at /usr/local/bin/tsc for all Node.js MCP servers
29+
RUN npm install -g typescript && \
30+
tsc --version
31+
2732
# Create mcp-cache base directory with proper ownership
2833
RUN mkdir -p /opt/deploystack/mcp-cache && \
2934
chown -R deploystack:deploystack /opt/deploystack

services/satellite/src/process/github-deployment.ts

Lines changed: 202 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -266,141 +266,73 @@ export class GitHubDeploymentHandler {
266266
temp_dir: tempDir
267267
}, 'Installing dependencies with npm install');
268268

269-
return new Promise((resolve, reject) => {
270-
const npmInstall = spawn('npm', ['install'], {
271-
cwd: tempDir,
272-
stdio: 'pipe'
273-
});
274-
275-
let stderr = '';
276-
277-
// Capture and emit stdout to backend
278-
npmInstall.stdout.on('data', (data) => {
279-
const output = data.toString().trim();
280-
if (output) {
281-
this.logBuffer.add({
282-
installation_id: installationId,
283-
team_id: teamId,
284-
user_id: userId,
285-
level: 'info',
286-
message: `[npm install] ${output}`,
287-
timestamp: new Date().toISOString()
288-
});
289-
}
290-
});
291-
292-
npmInstall.stderr.on('data', (data) => {
293-
const output = data.toString().trim();
294-
stderr += output;
295-
296-
// Also emit stderr as warn logs
297-
if (output) {
298-
this.logBuffer.add({
299-
installation_id: installationId,
300-
team_id: teamId,
301-
user_id: userId,
302-
level: 'warn',
303-
message: `[npm install] ${output}`,
304-
timestamp: new Date().toISOString()
305-
});
306-
}
307-
});
308-
309-
npmInstall.on('exit', (code) => {
310-
if (code === 0) {
311-
this.logger.info({
312-
operation: 'npm_install_success',
313-
temp_dir: tempDir
314-
}, 'Dependencies installed successfully');
315-
resolve();
316-
} else {
317-
this.logger.error({
318-
operation: 'npm_install_failed',
319-
temp_dir: tempDir,
320-
exit_code: code,
321-
stderr: stderr.substring(0, 500) // Limit stderr output
322-
}, `npm install failed with code ${code}`);
323-
324-
reject(new Error(`npm install failed with code ${code}: ${stderr.substring(0, 200)}`));
269+
// Use nsjail in production, direct spawn in development
270+
if (this.processSpawner.shouldUseNsjail()) {
271+
const result = await this.processSpawner.spawnBuildCommandWithNsjail(
272+
'npm',
273+
['install'],
274+
tempDir,
275+
{
276+
allowNetwork: true, // npm needs network for package downloads
277+
timeoutMs: 120000, // 2 minutes
278+
runtime: 'node'
325279
}
326-
});
327-
328-
npmInstall.on('error', (error) => {
329-
this.logger.error({
330-
operation: 'npm_install_error',
331-
temp_dir: tempDir,
332-
error: error.message
333-
}, 'npm install process error');
334-
335-
reject(new Error(`npm install process error: ${error.message}`));
336-
});
337-
});
338-
}
339-
340-
/**
341-
* Build package if build script exists
342-
*/
343-
async buildPackage(
344-
tempDir: string,
345-
installationId: string,
346-
teamId: string,
347-
userId?: string
348-
): Promise<void> {
349-
try {
350-
// Read package.json to check for build script
351-
const packageJsonPath = path.join(tempDir, 'package.json');
352-
const packageJsonContent = await fs.promises.readFile(packageJsonPath, 'utf8');
353-
const packageJson = JSON.parse(packageJsonContent);
354-
355-
// Check if there's a build script
356-
if (!packageJson.scripts?.build) {
357-
this.logger.debug({
358-
operation: 'npm_build_skip',
359-
temp_dir: tempDir
360-
}, 'No build script found, skipping build');
280+
);
361281

362-
// Emit log to backend so users know build was skipped
282+
// Emit logs to backend
283+
if (result.stdout) {
363284
this.logBuffer.add({
364285
installation_id: installationId,
365286
team_id: teamId,
366287
user_id: userId,
367288
level: 'info',
368-
message: '[npm build] No build script found, skipping build',
289+
message: `[npm install] ${result.stdout.substring(0, 1000)}`,
369290
timestamp: new Date().toISOString()
370291
});
292+
}
371293

372-
return;
294+
if (result.code !== 0) {
295+
this.logBuffer.add({
296+
installation_id: installationId,
297+
team_id: teamId,
298+
user_id: userId,
299+
level: 'error',
300+
message: `[npm install] ${result.stderr.substring(0, 500)}`,
301+
timestamp: new Date().toISOString()
302+
});
303+
throw new Error(`npm install failed with code ${result.code}: ${result.stderr.substring(0, 200)}`);
373304
}
374305

375-
this.logger.debug({
376-
operation: 'npm_build_start',
306+
this.logger.info({
307+
operation: 'npm_install_success',
377308
temp_dir: tempDir
378-
}, 'Building package with npm run build');
379-
309+
}, 'Dependencies installed successfully');
310+
} else {
311+
// Development mode - direct spawn
380312
return new Promise((resolve, reject) => {
381-
const npmBuild = spawn('npm', ['run', 'build'], {
313+
const npmInstall = spawn('npm', ['install'], {
382314
cwd: tempDir,
383315
stdio: 'pipe'
384316
});
385317

386318
let stderr = '';
387319

388320
// Capture and emit stdout to backend
389-
npmBuild.stdout.on('data', (data) => {
321+
npmInstall.stdout.on('data', (data) => {
390322
const output = data.toString().trim();
391323
if (output) {
392324
this.logBuffer.add({
393325
installation_id: installationId,
394326
team_id: teamId,
395327
user_id: userId,
396328
level: 'info',
397-
message: `[npm build] ${output}`,
329+
message: `[npm install] ${output}`,
398330
timestamp: new Date().toISOString()
399331
});
400332
}
401333
});
402334

403-
npmBuild.stderr.on('data', (data) => {
335+
npmInstall.stderr.on('data', (data) => {
404336
const output = data.toString().trim();
405337
stderr += output;
406338

@@ -411,41 +343,197 @@ export class GitHubDeploymentHandler {
411343
team_id: teamId,
412344
user_id: userId,
413345
level: 'warn',
414-
message: `[npm build] ${output}`,
346+
message: `[npm install] ${output}`,
415347
timestamp: new Date().toISOString()
416348
});
417349
}
418350
});
419351

420-
npmBuild.on('exit', (code) => {
352+
npmInstall.on('exit', (code) => {
421353
if (code === 0) {
422354
this.logger.info({
423-
operation: 'npm_build_success',
355+
operation: 'npm_install_success',
424356
temp_dir: tempDir
425-
}, 'Package built successfully');
357+
}, 'Dependencies installed successfully');
426358
resolve();
427359
} else {
428360
this.logger.error({
429-
operation: 'npm_build_failed',
361+
operation: 'npm_install_failed',
430362
temp_dir: tempDir,
431363
exit_code: code,
432-
stderr: stderr.substring(0, 500)
433-
}, `npm run build failed with code ${code}`);
364+
stderr: stderr.substring(0, 500) // Limit stderr output
365+
}, `npm install failed with code ${code}`);
434366

435-
reject(new Error(`npm run build failed with code ${code}: ${stderr.substring(0, 200)}`));
367+
reject(new Error(`npm install failed with code ${code}: ${stderr.substring(0, 200)}`));
436368
}
437369
});
438370

439-
npmBuild.on('error', (error) => {
371+
npmInstall.on('error', (error) => {
440372
this.logger.error({
441-
operation: 'npm_build_error',
373+
operation: 'npm_install_error',
442374
temp_dir: tempDir,
443375
error: error.message
444-
}, 'npm run build process error');
376+
}, 'npm install process error');
445377

446-
reject(new Error(`npm run build process error: ${error.message}`));
378+
reject(new Error(`npm install process error: ${error.message}`));
447379
});
448380
});
381+
}
382+
}
383+
384+
/**
385+
* Build package if build script exists
386+
*/
387+
async buildPackage(
388+
tempDir: string,
389+
installationId: string,
390+
teamId: string,
391+
userId?: string
392+
): Promise<void> {
393+
try {
394+
// Read package.json to check for build script
395+
const packageJsonPath = path.join(tempDir, 'package.json');
396+
const packageJsonContent = await fs.promises.readFile(packageJsonPath, 'utf8');
397+
const packageJson = JSON.parse(packageJsonContent);
398+
399+
// Check if there's a build script
400+
if (!packageJson.scripts?.build) {
401+
this.logger.debug({
402+
operation: 'npm_build_skip',
403+
temp_dir: tempDir
404+
}, 'No build script found, skipping build');
405+
406+
// Emit log to backend so users know build was skipped
407+
this.logBuffer.add({
408+
installation_id: installationId,
409+
team_id: teamId,
410+
user_id: userId,
411+
level: 'info',
412+
message: '[npm build] No build script found, skipping build',
413+
timestamp: new Date().toISOString()
414+
});
415+
416+
return;
417+
}
418+
419+
this.logger.debug({
420+
operation: 'npm_build_start',
421+
temp_dir: tempDir
422+
}, 'Building package with npm run build');
423+
424+
// Use nsjail in production, direct spawn in development
425+
if (this.processSpawner.shouldUseNsjail()) {
426+
const result = await this.processSpawner.spawnBuildCommandWithNsjail(
427+
'npm',
428+
['run', 'build'],
429+
tempDir,
430+
{
431+
allowNetwork: false, // Build should not need network
432+
timeoutMs: 120000, // 2 minutes
433+
runtime: 'node'
434+
}
435+
);
436+
437+
// Emit logs to backend
438+
if (result.stdout) {
439+
this.logBuffer.add({
440+
installation_id: installationId,
441+
team_id: teamId,
442+
user_id: userId,
443+
level: 'info',
444+
message: `[npm build] ${result.stdout.substring(0, 1000)}`,
445+
timestamp: new Date().toISOString()
446+
});
447+
}
448+
449+
if (result.code !== 0) {
450+
this.logBuffer.add({
451+
installation_id: installationId,
452+
team_id: teamId,
453+
user_id: userId,
454+
level: 'error',
455+
message: `[npm build] ${result.stderr.substring(0, 500)}`,
456+
timestamp: new Date().toISOString()
457+
});
458+
throw new Error(`npm run build failed with code ${result.code}: ${result.stderr.substring(0, 200)}`);
459+
}
460+
461+
this.logger.info({
462+
operation: 'npm_build_success',
463+
temp_dir: tempDir
464+
}, 'Package built successfully');
465+
} else {
466+
// Development mode - direct spawn
467+
return new Promise((resolve, reject) => {
468+
const npmBuild = spawn('npm', ['run', 'build'], {
469+
cwd: tempDir,
470+
stdio: 'pipe'
471+
});
472+
473+
let stderr = '';
474+
475+
// Capture and emit stdout to backend
476+
npmBuild.stdout.on('data', (data) => {
477+
const output = data.toString().trim();
478+
if (output) {
479+
this.logBuffer.add({
480+
installation_id: installationId,
481+
team_id: teamId,
482+
user_id: userId,
483+
level: 'info',
484+
message: `[npm build] ${output}`,
485+
timestamp: new Date().toISOString()
486+
});
487+
}
488+
});
489+
490+
npmBuild.stderr.on('data', (data) => {
491+
const output = data.toString().trim();
492+
stderr += output;
493+
494+
// Also emit stderr as warn logs
495+
if (output) {
496+
this.logBuffer.add({
497+
installation_id: installationId,
498+
team_id: teamId,
499+
user_id: userId,
500+
level: 'warn',
501+
message: `[npm build] ${output}`,
502+
timestamp: new Date().toISOString()
503+
});
504+
}
505+
});
506+
507+
npmBuild.on('exit', (code) => {
508+
if (code === 0) {
509+
this.logger.info({
510+
operation: 'npm_build_success',
511+
temp_dir: tempDir
512+
}, 'Package built successfully');
513+
resolve();
514+
} else {
515+
this.logger.error({
516+
operation: 'npm_build_failed',
517+
temp_dir: tempDir,
518+
exit_code: code,
519+
stderr: stderr.substring(0, 500)
520+
}, `npm run build failed with code ${code}`);
521+
522+
reject(new Error(`npm run build failed with code ${code}: ${stderr.substring(0, 200)}`));
523+
}
524+
});
525+
526+
npmBuild.on('error', (error) => {
527+
this.logger.error({
528+
operation: 'npm_build_error',
529+
temp_dir: tempDir,
530+
error: error.message
531+
}, 'npm run build process error');
532+
533+
reject(new Error(`npm run build process error: ${error.message}`));
534+
});
535+
});
536+
}
449537

450538
} catch (error) {
451539
const errorMessage = error instanceof Error ? error.message : String(error);

0 commit comments

Comments
 (0)