import cp from "child_process"; import express from "express"; import chalk from "chalk"; import path from "path"; import md5 from "md5"; import fs from "fs"; import os from "os"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; import crypto from "crypto"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const DIR = process.env.NODE_ENV === "development" ? path.join(__dirname, "../") : path.join(os.homedir(), ".ncicd"); if (process.env.NODE_ENV !== "development" && !fs.existsSync(DIR)) { fs.mkdirSync(DIR, { recursive: true }); } app.get("/", async (req, res) => { const password = req.query.password; const name = req.query.name; if (!name) { res.status(400).send("Access denied."); return; } console.log( chalk.cyan(`${name} - Pipeline is running`) ); const file = fs.readFileSync(path.join(DIR, "ncicd.conf")).toString(); const json = JSON.parse(file); const currentData = json.find(item => item.name === name); if (!currentData) { console.error(chalk.red(`Error: No configuration found for pipeline '${name}'`)); res.status(400).send(`Error: Pipeline '${name}' not found`); return; } const pass = password as string; let unencryptedKey = currentData.rsaKeys.privateKey; try { const privateKeyObj = crypto.createPrivateKey({ key: currentData.rsaKeys.privateKey, format: "pem", passphrase: pass }); unencryptedKey = privateKeyObj.export({ type: "pkcs1", format: "pem" }); } catch (e) { console.error(chalk.yellow("Note: Could not decrypt private key. It might already be unencrypted or the password is wrong.")); } const uniqueKeyFile = path.join(DIR, `privateKey_${name}_${Date.now()}.pem`); fs.writeFileSync(uniqueKeyFile, unencryptedKey, { mode: "600" }); fs.chmodSync(uniqueKeyFile, "0600"); try { await new Promise((resolve, reject) => { const gitPuller = cp.exec(`cd ${path.join(currentData.targetPath, currentData.name)} && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -i ${uniqueKeyFile}" git fetch origin ${currentData.branch} && git reset --hard FETCH_HEAD`, { maxBuffer: 1024 * 1024 * 500 }, (error, stdout, stderr) => { if (error) { console.error(chalk.red(`Git pull failed for ${name}:`), error); reject(error); } else { console.log(chalk.green(`Git pull successful for ${name}:`), stdout); resolve(); } }); gitPuller.stdout?.on("data", (data) => { const output = data.toString(); console.log(chalk.gray(`[git stdout]: ${output}`)); }); gitPuller.stderr?.on("data", (data) => { const output = data.toString(); console.log(chalk.gray(`[git stderr]: ${output}`)); if (output.toLowerCase().includes("passphrase")) { console.error(chalk.red("Incorrect password provided, SSH key could not be decrypted.")); gitPuller.kill(); } }); }); } catch (err) { res.status(500).send(`Failed to pull repository for ${name}`); return; } finally { if (fs.existsSync(uniqueKeyFile)) { fs.unlinkSync(uniqueKeyFile); } } const directory = path.join(currentData.targetPath, currentData.name); if (currentData.nodeVersion) { cp.execSync(`export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" && nvm install ${currentData.nodeVersion} && nvm use ${currentData.nodeVersion} && npm i -g yarn pm2 && cd ${directory} && yarn install --check-files --ignore-engines && yarn ${currentData.buildCommand ? currentData.buildCommand : "build"}`, { maxBuffer: 1024 * 1024 * 50, stdio: "inherit" }); } else { cp.execSync(`cd ${directory} && yarn install --ignore-engines && yarn ${currentData.buildCommand ? currentData.buildCommand : "build"}`, { maxBuffer: 1024 * 1024 * 20, stdio: "inherit" }); } let pm2RestartCommand = `npx pm2 restart ${currentData.name} --update-env`; let pm2StartCommand = `cd ${directory} && npx pm2 start yarn --name ${currentData.name} --time --log-date-format "YYYY-MM-DDTHH:mm:ss.SSSZ" -l ${currentData.name}-combined.log -- ${currentData.command ? currentData.command : "start:prod"}`; if (currentData.nodeVersion) { pm2RestartCommand = `export NVM_DIR="$HOME/.nvm"\n[ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" && nvm use ${currentData.nodeVersion} && ${pm2RestartCommand}`; pm2StartCommand = `export NVM_DIR="$HOME/.nvm"\n[ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" && nvm use ${currentData.nodeVersion} && ${pm2StartCommand}`; } try { cp.execSync(pm2RestartCommand); } catch { cp.execSync(pm2StartCommand); } if (process.platform !== "win32") { cp.execSync("pm2 startup"); } cp.execSync("pm2 save"); console.log( chalk.green(`${name} - Pipeline is completed successfully`) ); res.send(`${name} - OK`); return; }); app.post("/", async (req, res) => { const password = req.query.password; const name = req.query.name; if (!name) { res.status(403).send("Access denied."); return; } console.log( chalk.cyan(`${name} - Pipeline is running`) ); const file = fs.readFileSync(path.join(DIR, "ncicd.conf")).toString(); const json = JSON.parse(file); const currentData = json.find(item => item.name === name); if (!currentData) { console.error(chalk.red(`Error: No configuration found for pipeline '${name}'`)); res.status(400).send(`Error: Pipeline '${name}' not found`); return; } const pass = password as string; let unencryptedKey = currentData.rsaKeys.privateKey; try { const privateKeyObj = crypto.createPrivateKey({ key: currentData.rsaKeys.privateKey, format: "pem", passphrase: pass }); unencryptedKey = privateKeyObj.export({ type: "pkcs1", format: "pem" }); } catch (e) { console.error(chalk.yellow("Note: Could not decrypt private key. It might already be unencrypted or the password is wrong.")); } const uniqueKeyFile = path.join(DIR, `privateKey_${name}_${Date.now()}.pem`); fs.writeFileSync(uniqueKeyFile, unencryptedKey, { mode: "600" }); fs.chmodSync(uniqueKeyFile, "0600"); try { await new Promise((resolve, reject) => { const gitPuller = cp.exec(`cd ${path.join(currentData.targetPath, currentData.name)} && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -i ${uniqueKeyFile}" git fetch origin ${currentData.branch} && git reset --hard FETCH_HEAD`, { maxBuffer: 1024 * 1024 * 500 }, (error, stdout, stderr) => { if (error) { console.error(chalk.red(`Git pull failed for ${name}:`), error); reject(error); } else { console.log(chalk.green(`Git pull successful for ${name}:`), stdout); resolve(); } }); gitPuller.stdout?.on("data", (data) => { const output = data.toString(); console.log(chalk.gray(`[git stdout]: ${output}`)); }); gitPuller.stderr?.on("data", (data) => { const output = data.toString(); console.log(chalk.gray(`[git stderr]: ${output}`)); if (output.toLowerCase().includes("passphrase")) { console.error(chalk.red("Incorrect password provided, SSH key could not be decrypted.")); gitPuller.kill(); } }); }); } catch (err) { res.status(500).send(`Failed to pull repository for ${name}`); return; } finally { if (fs.existsSync(uniqueKeyFile)) { fs.unlinkSync(uniqueKeyFile); } } const directory = path.join(currentData.targetPath, currentData.name); if (currentData.nodeVersion) { cp.execSync(`export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" && nvm install ${currentData.nodeVersion} && nvm use ${currentData.nodeVersion} && npm i -g yarn pm2 && cd ${directory} && yarn install --check-files --ignore-engines && yarn ${currentData.buildCommand ? currentData.buildCommand : "build"}`, { maxBuffer: 1024 * 1024 * 50, stdio: "inherit" }); } else { cp.execSync(`cd ${directory} && yarn && yarn ${currentData.buildCommand ? currentData.buildCommand : "build"}`, { maxBuffer: 1024 * 1024 * 20, stdio: "inherit" }); } let pm2RestartCommand2 = `npx pm2 restart ${currentData.name} --update-env`; let pm2StartCommand2 = `cd ${directory} && npx pm2 start yarn --name ${currentData.name} --time --log-date-format "YYYY-MM-DDTHH:mm:ss.SSSZ" -l ${currentData.name}-combined.log -- ${currentData.command ? currentData.command : "start:prod"}`; if (currentData.nodeVersion) { pm2RestartCommand2 = `export NVM_DIR="$HOME/.nvm"\n[ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" && nvm use ${currentData.nodeVersion} && ${pm2RestartCommand2}`; pm2StartCommand2 = `export NVM_DIR="$HOME/.nvm"\n[ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" && nvm use ${currentData.nodeVersion} && ${pm2StartCommand2}`; } try { cp.execSync(pm2RestartCommand2); } catch { cp.execSync(pm2StartCommand2); } if (process.platform !== "win32") { cp.execSync("pm2 startup"); } cp.execSync("pm2 save"); console.log( chalk.green(`${name} - Pipeline is completed successfully`) ); res.send(`${name} - OK`); return; }); const ncicdJSON = JSON.parse(fs.readFileSync(path.join(DIR, "ncicd.conf.json")).toString()); app.listen(ncicdJSON.PORT, ncicdJSON.bind, () => { console.log("System working on", ncicdJSON.bind, ":", ncicdJSON.PORT); });