Compare commits

..

10 Commits

Author SHA1 Message Date
Benjamin Toby
a3e20f15ef Updates 2025-01-16 06:25:16 +01:00
Benjamin Toby
09e8b1f6d7 Refactor to typescript 2025-01-16 06:22:33 +01:00
Benjamin Toby
5f589f89d8 Add port array and postflight 2023-11-06 21:06:33 +01:00
Benjamin Toby
bb3d0712ca Upgrades 2023-11-01 06:01:17 +01:00
Benjamin Toby
da71e853bc Bugfix 2023-10-29 15:54:24 +01:00
Benjamin Toby
0bae517554 Bugfix 2023-10-29 13:27:53 +01:00
Benjamin Toby
e75d08ca10 Bugfix 2023-10-29 13:26:29 +01:00
Benjamin Toby
8816b0292b Remove automatic first run 2023-10-29 13:04:21 +01:00
Benjamin Toby
c7096db568 Update preflight fail process 2023-10-29 12:36:35 +01:00
Benjamin Toby
50472dc67f Version 1.0.0 Tentative 2023-10-29 11:49:04 +01:00
38 changed files with 866 additions and 245 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules
/node_modules
/test

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/
//git.tben.me/api/packages/moduletrace/npm/:_authToken=${GITBEN_NPM_TOKEN}

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"files.associations": {
"ostream": "cpp"
},
"[python]": {
"editor.defaultFormatter": "ms-python.autopep8"
},
"python.formatting.provider": "none"
}

View File

@ -1,5 +1,10 @@
# Simple CI/CD package for any application
[![package version](https://img.shields.io/npm/v/nodecid.svg?style=flat-square)](https://npmjs.org/package/nodecid)
[![package downloads](https://img.shields.io/npm/dm/nodecid.svg?style=flat-square)](https://npmjs.org/package/nodecid)
[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
[![package license](https://img.shields.io/npm/l/nodecid.svg?style=flat-square)](https://npmjs.org/package/nodecid)
Integrate a simple CI/CD process into your application without the hassle.
_**NOTE:** This package needs `node` installed to work_
@ -90,6 +95,15 @@ _NOTE:_ This also works for other languages, example:
This app just runs whatever command you send it in an isolated child process, the command will be run as if being run in a terminal.
#### All Available options in `nodecid.config.json` file
- **`start`**: _string_: The start Command
- **`preflight`**: _string | Array_: Array of commands or shell script file to run before reloading application
- **`postflight`**: _string | Array_: _Optional_: Array of commands or shell script file to run after reloading application
- **`redeploy_path`**: _string_: _Optional_: The path to trigger a redeployment. Default `./REDEPLOY`
- **`port`**: _string | number | (string | number)[]_: _Optional_: A port(or array of ports) to kill if running a server. _NOTE_: it is important to provide this option if running a server else the process may not terminate properly
- **`first_run`**: _boolean_: _Optional_: If the preflight should run on first run. Default `false`.
### Redeployment
For continuos deployment and integration there needs to be a text file located in your project which the application can watch. Any time the content of this file is changed the application will rebuild and rerun your `start` command.

BIN
bin/nodecid Executable file

Binary file not shown.

BIN
bun.lockb Executable file

Binary file not shown.

View File

@ -3,7 +3,17 @@
"type": "object",
"properties": {
"start": {
"type": "string"
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
},
"preflight": {
"anyOf": [
@ -20,6 +30,9 @@
},
"redeploy_path": {
"type": "string"
},
"first_run": {
"type": "boolean"
}
},
"required": ["start", "preflight"]

View File

@ -1,143 +0,0 @@
// @ts-check
const path = require("path");
const fs = require("fs");
const {
execSync,
spawnSync,
spawn,
execFile,
execFileSync,
ChildProcess,
} = require("child_process");
const colors = require("../utils/console-colors");
////////////////////////////////////////////
////////////////////////////////////////////
////////////////////////////////////////////
/**
* # Start the process
* @param {object} param0
* @param {string} param0.command
* @param {string[] | string} param0.preflight
* @param {string} param0.redeploy_file
*/
function startProcess({ command, preflight, redeploy_file }) {
/** @type {ChildProcess | null} */
let childProcess = null;
try {
const runPreflight = preflightFn(preflight);
if (!preflight) {
process.exit();
}
childProcess = run(command);
if (!childProcess) {
console.log(
`${colors.FgRed}Error:${colors.Reset} Process couldn't start. Exiting...`
);
process.exit();
}
} catch (/** @type {*} */ error) {
console.log(
`${colors.FgRed}Error:${colors.Reset} First run failed! => ${error.message}`
);
}
fs.watchFile(redeploy_file, { interval: 1000 }, (curr, prev) => {
if (childProcess) {
console.log("Rebuilding ...");
try {
const runPreflight = preflightFn(preflight);
if (!preflight) {
process.exit();
}
childProcess.kill("SIGTERM");
} catch (/** @type {*} */ error) {
console.log(
`${colors.FgRed}Error:${colors.Reset} killing child processes => ${error.message}`
);
}
childProcess = run(command);
}
});
}
////////////////////////////////////////////
////////////////////////////////////////////
////////////////////////////////////////////
/**
* ## Preflight Function
* @param {string} command
* @returns {ChildProcess | null}
*/
function run(command) {
const startCommandArray = command.split(" ").filter((str) => str.trim());
try {
const firstCommand = startCommandArray.shift()?.[0];
if (!firstCommand) {
throw new Error("No Starting Command Found in command string!");
}
let childProcess = spawn(firstCommand, ["server.js"], {
cwd: process.cwd(),
stdio: "inherit",
});
return childProcess;
} catch (/** @type {*} */ error) {
console.log(
`${colors.FgRed}Error:${colors.Reset} running start command => ${error.message}`
);
return null;
}
}
////////////////////////////////////////////
////////////////////////////////////////////
////////////////////////////////////////////
/**
* ## Preflight Function
* @param {string[] | string} preflight
* @returns {boolean}
*/
function preflightFn(preflight) {
console.log("Preflight Running ...");
/** @type {import("child_process").ExecSyncOptions} */
const options = {
cwd: process.cwd(),
stdio: "inherit",
};
try {
if (typeof preflight == "string") {
execFileSync(preflight, options);
} else if (typeof preflight == "object" && preflight?.[0]) {
preflight.forEach((cmd) => execSync(cmd, options));
}
return true;
} catch (error) {
console.log(
`${colors.FgRed}Error:${colors.Reset} Preflight Failed! => ${error.message}`
);
return false;
}
}
////////////////////////////////////////////
////////////////////////////////////////////
////////////////////////////////////////////
module.exports = startProcess;

1
dist/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export {};

50
dist/index.js vendored Normal file
View File

@ -0,0 +1,50 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const console_colors_1 = __importDefault(require("./utils/console-colors"));
const start_1 = __importDefault(require("./utils/start"));
const WORK_DIR = process.cwd();
function run() {
try {
const configText = fs_1.default.readFileSync(path_1.default.join(WORK_DIR, "nodecid.config.json"), "utf-8");
const config = JSON.parse(configText);
const { start, preflight, postflight, build, redeploy_path, first_run, port, } = config;
/** @type {string | undefined} */
let redeployFile;
if (!redeploy_path) {
const defaultRedeployPath = path_1.default.join(WORK_DIR, "REDEPLOY");
const checkExistingPath = fs_1.default.existsSync(defaultRedeployPath);
if (!checkExistingPath) {
fs_1.default.writeFileSync(defaultRedeployPath, Date.now().toString(), "utf-8");
}
redeployFile = path_1.default.join(WORK_DIR, "REDEPLOY");
}
else {
redeployFile = path_1.default.resolve(WORK_DIR, redeploy_path);
}
if (!redeployFile)
throw new Error("Redeploy file not found!");
(0, start_1.default)({
command: start,
preflight,
redeploy_file: redeployFile,
first_run,
port,
postflight,
});
}
catch (error) {
console.log(`${console_colors_1.default.FgRed}ERROR:${console_colors_1.default.Reset} CI process failed! => ${error.message}`);
}
}
run();
process.on("exit", () => {
console.log("Process exiting ...");
});
process.on("beforeExit", () => {
console.log("Process Before exit ...");
});

1
dist/tsconfig.tsbuildinfo vendored Normal file

File diff suppressed because one or more lines are too long

13
dist/types.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
export interface NodeCIConfig {
start: string;
preflight: string[] | string;
postflight?: string[] | string;
redeploy_path?: string;
first_run?: boolean;
port?: string | number | (string | number)[];
build?: NodeCIBuild;
}
export interface NodeCIBuild {
paradigm: "Next.JS" | "Remix";
out_dir?: string;
}

2
dist/types.js vendored Normal file
View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

28
dist/utils/console-colors.d.ts vendored Normal file
View File

@ -0,0 +1,28 @@
declare const colors: {
Reset: string;
Bright: string;
Dim: string;
Underscore: string;
Blink: string;
Reverse: string;
Hidden: string;
FgBlack: string;
FgRed: string;
FgGreen: string;
FgYellow: string;
FgBlue: string;
FgMagenta: string;
FgCyan: string;
FgWhite: string;
FgGray: string;
BgBlack: string;
BgRed: string;
BgGreen: string;
BgYellow: string;
BgBlue: string;
BgMagenta: string;
BgCyan: string;
BgWhite: string;
BgGray: string;
};
export default colors;

61
utils/console-colors.js → dist/utils/console-colors.js vendored Executable file → Normal file
View File

@ -1,31 +1,30 @@
const colors = {
Reset: "\x1b[0m",
Bright: "\x1b[1m",
Dim: "\x1b[2m",
Underscore: "\x1b[4m",
Blink: "\x1b[5m",
Reverse: "\x1b[7m",
Hidden: "\x1b[8m",
FgBlack: "\x1b[30m",
FgRed: "\x1b[31m",
FgGreen: "\x1b[32m",
FgYellow: "\x1b[33m",
FgBlue: "\x1b[34m",
FgMagenta: "\x1b[35m",
FgCyan: "\x1b[36m",
FgWhite: "\x1b[37m",
FgGray: "\x1b[90m",
BgBlack: "\x1b[40m",
BgRed: "\x1b[41m",
BgGreen: "\x1b[42m",
BgYellow: "\x1b[43m",
BgBlue: "\x1b[44m",
BgMagenta: "\x1b[45m",
BgCyan: "\x1b[46m",
BgWhite: "\x1b[47m",
BgGray: "\x1b[100m",
};
module.exports = colors;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const colors = {
Reset: "\x1b[0m",
Bright: "\x1b[1m",
Dim: "\x1b[2m",
Underscore: "\x1b[4m",
Blink: "\x1b[5m",
Reverse: "\x1b[7m",
Hidden: "\x1b[8m",
FgBlack: "\x1b[30m",
FgRed: "\x1b[31m",
FgGreen: "\x1b[32m",
FgYellow: "\x1b[33m",
FgBlue: "\x1b[34m",
FgMagenta: "\x1b[35m",
FgCyan: "\x1b[36m",
FgWhite: "\x1b[37m",
FgGray: "\x1b[90m",
BgBlack: "\x1b[40m",
BgRed: "\x1b[41m",
BgGreen: "\x1b[42m",
BgYellow: "\x1b[43m",
BgBlue: "\x1b[44m",
BgMagenta: "\x1b[45m",
BgCyan: "\x1b[46m",
BgWhite: "\x1b[47m",
BgGray: "\x1b[100m",
};
exports.default = colors;

6
dist/utils/kill-child.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/**
* ## Kill Child Process Function
* @param {string | number | (string | number)[]} [port]
* @returns {Promise<boolean>}
*/
export default function killChild(port?: string | number | (string | number)[]): Promise<boolean>;

47
dist/utils/kill-child.js vendored Normal file
View File

@ -0,0 +1,47 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = killChild;
const console_colors_1 = __importDefault(require("./console-colors"));
const kill_port_1 = __importDefault(require("kill-port"));
let childProcess = null;
/**
* ## Kill Child Process Function
* @param {string | number | (string | number)[]} [port]
* @returns {Promise<boolean>}
*/
function killChild(port) {
return __awaiter(this, void 0, void 0, function* () {
if (!childProcess)
return false;
try {
const childProcessPID = childProcess.pid;
childProcess.kill();
if (typeof port == "object" && (port === null || port === void 0 ? void 0 : port[0])) {
for (let i = 0; i < port.length; i++) {
const singlePort = port[i];
yield (0, kill_port_1.default)(Number(singlePort));
}
}
else if (port) {
yield (0, kill_port_1.default)(Number(port));
}
return true;
}
catch (error) {
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} Child Process couldn't be killed! ${error.message}`);
return false;
}
});
}

7
dist/utils/preflight.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
/**
* ## Preflight Function
* @param {string[] | string} preflight
* @param {boolean} [postflight]
* @returns {boolean}
*/
export default function preflightFn(preflight?: string[] | string, postflight?: boolean): boolean;

45
dist/utils/preflight.js vendored Normal file
View File

@ -0,0 +1,45 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = preflightFn;
const child_process_1 = require("child_process");
const console_colors_1 = __importDefault(require("../utils/console-colors"));
/**
* ## Preflight Function
* @param {string[] | string} preflight
* @param {boolean} [postflight]
* @returns {boolean}
*/
function preflightFn(preflight, postflight) {
const tag = postflight ? "Postflight" : "Preflight";
console.log(`${tag} Running ...`);
const options = {
cwd: process.cwd(),
stdio: "inherit",
};
try {
if (typeof preflight == "string") {
(0, child_process_1.execFileSync)(preflight, options);
}
else if (typeof preflight == "object" && (preflight === null || preflight === void 0 ? void 0 : preflight[0])) {
for (let i = 0; i < preflight.length; i++) {
const cmd = preflight[i];
try {
const execCmd = (0, child_process_1.execSync)(cmd, options);
}
catch (error) {
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} ${tag} command ${cmd} Failed! => ${error.message}`);
return false;
break;
}
}
}
return true;
}
catch (error) {
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} ${tag} Failed! => ${error.message}`);
return false;
}
}

7
dist/utils/run.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import { ChildProcess } from "child_process";
/**
* ## Preflight Function
* @param {string} command
* @returns {ChildProcess | null}
*/
export default function run(command: string): ChildProcess | null;

37
dist/utils/run.js vendored Normal file
View File

@ -0,0 +1,37 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = run;
const child_process_1 = require("child_process");
const console_colors_1 = __importDefault(require("./console-colors"));
let redeployments = 0;
const KILL_SIGNAL = "SIGTERM";
/**
* ## Preflight Function
* @param {string} command
* @returns {ChildProcess | null}
*/
function run(command) {
console.log("\n******************************");
console.log(`****** ${console_colors_1.default.FgGreen}Starting App ...${console_colors_1.default.Reset} ******`);
console.log("******************************\n");
const startCommandArray = command.split(" ").filter((str) => str.trim());
try {
const firstCommand = startCommandArray.shift();
if (!firstCommand) {
throw new Error("No Starting Command Found in command string!");
}
let childProcess = (0, child_process_1.spawn)(firstCommand, startCommandArray, {
stdio: "inherit",
killSignal: KILL_SIGNAL,
});
redeployments++;
return childProcess;
}
catch (error) {
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} running start command => ${error.message}`);
return null;
}
}

18
dist/utils/start.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
/**
* # Start the process
* @param {object} param0
* @param {string} param0.command
* @param {string[] | string} param0.preflight
* @param {string[] | string} [param0.postflight]
* @param {string} param0.redeploy_file
* @param {string | number | (string | number)[]} [param0.port] - The port to kill on rebuild
* @param {boolean} [param0.first_run] - Whether to run the preflight on first run. Default `false`
*/
export default function startProcess({ command, preflight, postflight, redeploy_file, port, first_run, }: {
command: string;
preflight: string[] | string;
postflight?: string[] | string;
redeploy_file: string;
port?: string | number | (string | number)[];
first_run?: boolean;
}): void;

84
dist/utils/start.js vendored Normal file
View File

@ -0,0 +1,84 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = startProcess;
const fs_1 = __importDefault(require("fs"));
const console_colors_1 = __importDefault(require("./console-colors"));
const preflight_1 = __importDefault(require("./preflight"));
const run_1 = __importDefault(require("./run"));
const kill_child_1 = __importDefault(require("./kill-child"));
let redeployments = 0;
let childProcess = null;
const pTitle = "nodecid";
process.title = pTitle;
/**
* # Start the process
* @param {object} param0
* @param {string} param0.command
* @param {string[] | string} param0.preflight
* @param {string[] | string} [param0.postflight]
* @param {string} param0.redeploy_file
* @param {string | number | (string | number)[]} [param0.port] - The port to kill on rebuild
* @param {boolean} [param0.first_run] - Whether to run the preflight on first run. Default `false`
*/
function startProcess({ command, preflight, postflight, redeploy_file, port, first_run, }) {
try {
if (first_run) {
console.log("First Run ...");
const runPreflight = (0, preflight_1.default)(preflight);
}
if (!preflight) {
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} No preflight included in config file. If you don't want to run any preflight command simply add an empty array.`);
process.exit();
}
childProcess = (0, run_1.default)(command);
if (!childProcess) {
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} Process couldn't start. Exiting...`);
process.exit();
}
console.log("Watching", redeploy_file);
fs_1.default.watchFile(redeploy_file, { interval: 100 }, (curr, prev) => {
console.log(`${console_colors_1.default.BgBlue}File Changed${console_colors_1.default.Reset}`);
if (redeployments == 0)
return;
if (childProcess) {
console.log("******************************");
console.log(`******** ${console_colors_1.default.FgBlue}Rebuilding ${console_colors_1.default.FgMagenta}${redeployments}${console_colors_1.default.Reset} ********`);
console.log("******************************");
try {
const runPreflight = (0, preflight_1.default)(preflight);
if (!runPreflight) {
// TODO: Action to take if preflight fails
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} Preflight Failed.`);
}
else {
(0, kill_child_1.default)(port).then((kill) => {
if (kill) {
childProcess = (0, run_1.default)(command);
if (postflight) {
const runPostflight = (0, preflight_1.default)(postflight, true);
if (!runPostflight) {
// TODO: Action to take if postflight fails
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} Postflight Failed.`);
}
}
}
else {
process.exit();
}
});
}
}
catch (error) {
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} killing child processes => ${error.message}`);
process.exit();
}
}
});
}
catch (error) {
console.log(`${console_colors_1.default.FgRed}Error:${console_colors_1.default.Reset} First run failed! => ${error.message}`);
}
}

1
dist/utils/triggers/github.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare function githubWebhook(): boolean;

View File

@ -1,3 +1,4 @@
"use strict";
function githubWebhook() {
return true;
}

View File

@ -1,56 +0,0 @@
#! /usr/bin/env node
// @ts-check
const fs = require("fs");
const path = require("path");
const colors = require("./utils/console-colors");
const startProcess = require("./deploy/start");
///////////////////////////////////////////////
///////////////////////////////////////////////
///////////////////////////////////////////////
const WORK_DIR = process.cwd();
///////////////////////////////////////////////
///////////////////////////////////////////////
///////////////////////////////////////////////
try {
const configText = fs.readFileSync(
path.join(WORK_DIR, "nodecid.config.json"),
"utf-8"
);
/** @type {NodeCIConfig} */
const config = JSON.parse(configText);
const { start, preflight, redeploy_path } = config;
/** @type {string | undefined} */
let redeployFile;
if (!redeploy_path) {
fs.writeFileSync(
path.join(WORK_DIR, "REDEPLY"),
Date.now().toString(),
"utf-8"
);
redeployFile = path.join(WORK_DIR, "REDEPLY");
} else {
redeployFile = path.resolve(WORK_DIR, redeploy_path);
}
if (!redeployFile) throw new Error("Redeploy file not found!");
startProcess({
command: start,
preflight,
redeploy_file: redeployFile,
});
} catch (error) {
console.log(
`${colors.FgRed}ERROR:${colors.Reset} CI process failed! => ${error.message}`
);
}

73
index.ts Executable file
View File

@ -0,0 +1,73 @@
import fs from "fs";
import path from "path";
import colors from "./utils/console-colors";
import startProcess from "./utils/start";
import type { NodeCIConfig } from "./types";
const WORK_DIR = process.cwd();
function run() {
try {
const configText = fs.readFileSync(
path.join(WORK_DIR, "nodecid.config.json"),
"utf-8"
);
const config: NodeCIConfig = JSON.parse(configText);
const {
start,
preflight,
postflight,
build,
redeploy_path,
first_run,
port,
} = config;
/** @type {string | undefined} */
let redeployFile;
if (!redeploy_path) {
const defaultRedeployPath = path.join(WORK_DIR, "REDEPLOY");
const checkExistingPath = fs.existsSync(defaultRedeployPath);
if (!checkExistingPath) {
fs.writeFileSync(
defaultRedeployPath,
Date.now().toString(),
"utf-8"
);
}
redeployFile = path.join(WORK_DIR, "REDEPLOY");
} else {
redeployFile = path.resolve(WORK_DIR, redeploy_path);
}
if (!redeployFile) throw new Error("Redeploy file not found!");
startProcess({
command: start,
preflight,
redeploy_file: redeployFile,
first_run,
port,
postflight,
});
} catch (error: any) {
console.log(
`${colors.FgRed}ERROR:${colors.Reset} CI process failed! => ${error.message}`
);
}
}
run();
process.on("exit", () => {
console.log("Process exiting ...");
});
process.on("beforeExit", () => {
console.log("Process Before exit ...");
});

27
jsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@ -1,11 +1,14 @@
{
"name": "nodecid",
"version": "1.0.0",
"name": "@moduletrace/nodecid",
"version": "1.0.7",
"description": "Simple CI/CD process",
"main": "index.js",
"main": "dist/index.js",
"bin": {
"nodecid": "./index.js",
"node-ci-cd": "./index.js"
"nodecid": "./dist/index.js",
"node-ci-cd": "./dist/index.js"
},
"scripts": {
"compile": "bun build --compile --minify --sourcemap --bytecode index.ts --outfile bin/nodecid"
},
"keywords": [
"CI/CD",
@ -13,5 +16,15 @@
"Continous Deployment"
],
"author": "Benjamin Toby",
"license": "MIT"
"license": "MIT",
"dependencies": {
"kill-port": "^2.0.1"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^22.10.7"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}

14
publish.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
tsc
if [ -z "$1" ]; then
msg="Updates"
else
msg="$1"
fi
git add .
git commit -m "$msg"
git push
npm publish

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"maxNodeModuleJsDepth": 10,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"incremental": true,
"resolveJsonModule": true,
"jsx": "preserve",
"moduleResolution": "node",
"declaration": true,
"outDir": "dist"
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,8 +0,0 @@
/**
* @typedef {object} NodeCIConfig
* @property {string} start - Start command. Eg `node index.js`
* @property {string[] | string} preflight - And array of commands to run before
* the application starts, or a single `.sh` file path.
* @property {string} [redeploy_path] - The path to the file that will trigger a
* redeployment if content is changed. Default file path is `./REDEPLOY`
*/

14
types.ts Normal file
View File

@ -0,0 +1,14 @@
export interface NodeCIConfig {
start: string;
preflight: string[] | string;
postflight?: string[] | string;
redeploy_path?: string;
first_run?: boolean;
port?: string | number | (string | number)[];
build?: NodeCIBuild;
}
export interface NodeCIBuild {
paradigm: "Next.JS" | "Remix";
out_dir?: string;
}

31
utils/console-colors.ts Executable file
View File

@ -0,0 +1,31 @@
const colors = {
Reset: "\x1b[0m",
Bright: "\x1b[1m",
Dim: "\x1b[2m",
Underscore: "\x1b[4m",
Blink: "\x1b[5m",
Reverse: "\x1b[7m",
Hidden: "\x1b[8m",
FgBlack: "\x1b[30m",
FgRed: "\x1b[31m",
FgGreen: "\x1b[32m",
FgYellow: "\x1b[33m",
FgBlue: "\x1b[34m",
FgMagenta: "\x1b[35m",
FgCyan: "\x1b[36m",
FgWhite: "\x1b[37m",
FgGray: "\x1b[90m",
BgBlack: "\x1b[40m",
BgRed: "\x1b[41m",
BgGreen: "\x1b[42m",
BgYellow: "\x1b[43m",
BgBlue: "\x1b[44m",
BgMagenta: "\x1b[45m",
BgCyan: "\x1b[46m",
BgWhite: "\x1b[47m",
BgGray: "\x1b[100m",
};
export default colors;

37
utils/kill-child.ts Executable file
View File

@ -0,0 +1,37 @@
import { ChildProcess } from "child_process";
import colors from "./console-colors";
import kill from "kill-port";
let childProcess: ChildProcess | null = null;
/**
* ## Kill Child Process Function
* @param {string | number | (string | number)[]} [port]
* @returns {Promise<boolean>}
*/
export default async function killChild(
port?: string | number | (string | number)[]
): Promise<boolean> {
if (!childProcess) return false;
try {
const childProcessPID = childProcess.pid;
childProcess.kill();
if (typeof port == "object" && port?.[0]) {
for (let i = 0; i < port.length; i++) {
const singlePort = port[i];
await kill(Number(singlePort));
}
} else if (port) {
await kill(Number(port));
}
return true;
} catch (error: any) {
console.log(
`${colors.FgRed}Error:${colors.Reset} Child Process couldn't be killed! ${error.message}`
);
return false;
}
}

46
utils/preflight.ts Executable file
View File

@ -0,0 +1,46 @@
import { execSync, execFileSync, type ExecSyncOptions } from "child_process";
import colors from "../utils/console-colors";
/**
* ## Preflight Function
* @param {string[] | string} preflight
* @param {boolean} [postflight]
* @returns {boolean}
*/
export default function preflightFn(
preflight?: string[] | string,
postflight?: boolean
): boolean {
const tag = postflight ? "Postflight" : "Preflight";
console.log(`${tag} Running ...`);
const options: ExecSyncOptions = {
cwd: process.cwd(),
stdio: "inherit",
};
try {
if (typeof preflight == "string") {
execFileSync(preflight, options);
} else if (typeof preflight == "object" && preflight?.[0]) {
for (let i = 0; i < preflight.length; i++) {
const cmd = preflight[i];
try {
const execCmd = execSync(cmd, options);
} catch (error: any) {
console.log(
`${colors.FgRed}Error:${colors.Reset} ${tag} command ${cmd} Failed! => ${error.message}`
);
return false;
break;
}
}
}
return true;
} catch (error: any) {
console.log(
`${colors.FgRed}Error:${colors.Reset} ${tag} Failed! => ${error.message}`
);
return false;
}
}

43
utils/run.ts Executable file
View File

@ -0,0 +1,43 @@
import { spawn, ChildProcess } from "child_process";
import colors from "./console-colors";
let redeployments = 0;
const KILL_SIGNAL: NodeJS.Signals | number = "SIGTERM";
/**
* ## Preflight Function
* @param {string} command
* @returns {ChildProcess | null}
*/
export default function run(command: string): ChildProcess | null {
console.log("\n******************************");
console.log(
`****** ${colors.FgGreen}Starting App ...${colors.Reset} ******`
);
console.log("******************************\n");
const startCommandArray = command.split(" ").filter((str) => str.trim());
try {
const firstCommand = startCommandArray.shift();
if (!firstCommand) {
throw new Error("No Starting Command Found in command string!");
}
let childProcess = spawn(firstCommand, startCommandArray, {
stdio: "inherit",
killSignal: KILL_SIGNAL,
});
redeployments++;
return childProcess;
} catch (error: any) {
console.log(
`${colors.FgRed}Error:${colors.Reset} running start command => ${error.message}`
);
return null;
}
}

123
utils/start.ts Executable file
View File

@ -0,0 +1,123 @@
import fs from "fs";
import { ChildProcess } from "child_process";
import colors from "./console-colors";
import kill from "kill-port";
import preflightFn from "./preflight";
import run from "./run";
import killChild from "./kill-child";
let redeployments = 0;
let childProcess: ChildProcess | null = null;
const pTitle = "nodecid";
process.title = pTitle;
/**
* # Start the process
* @param {object} param0
* @param {string} param0.command
* @param {string[] | string} param0.preflight
* @param {string[] | string} [param0.postflight]
* @param {string} param0.redeploy_file
* @param {string | number | (string | number)[]} [param0.port] - The port to kill on rebuild
* @param {boolean} [param0.first_run] - Whether to run the preflight on first run. Default `false`
*/
export default function startProcess({
command,
preflight,
postflight,
redeploy_file,
port,
first_run,
}: {
command: string;
preflight: string[] | string;
postflight?: string[] | string;
redeploy_file: string;
port?: string | number | (string | number)[];
first_run?: boolean;
}) {
try {
if (first_run) {
console.log("First Run ...");
const runPreflight = preflightFn(preflight);
}
if (!preflight) {
console.log(
`${colors.FgRed}Error:${colors.Reset} No preflight included in config file. If you don't want to run any preflight command simply add an empty array.`
);
process.exit();
}
childProcess = run(command);
if (!childProcess) {
console.log(
`${colors.FgRed}Error:${colors.Reset} Process couldn't start. Exiting...`
);
process.exit();
}
console.log("Watching", redeploy_file);
fs.watchFile(redeploy_file, { interval: 100 }, (curr, prev) => {
console.log(`${colors.BgBlue}File Changed${colors.Reset}`);
if (redeployments == 0) return;
if (childProcess) {
console.log("******************************");
console.log(
`******** ${colors.FgBlue}Rebuilding ${colors.FgMagenta}${redeployments}${colors.Reset} ********`
);
console.log("******************************");
try {
const runPreflight = preflightFn(preflight);
if (!runPreflight) {
// TODO: Action to take if preflight fails
console.log(
`${colors.FgRed}Error:${colors.Reset} Preflight Failed.`
);
} else {
killChild(port).then((kill) => {
if (kill) {
childProcess = run(command);
if (postflight) {
const runPostflight = preflightFn(
postflight,
true
);
if (!runPostflight) {
// TODO: Action to take if postflight fails
console.log(
`${colors.FgRed}Error:${colors.Reset} Postflight Failed.`
);
}
}
} else {
process.exit();
}
});
}
} catch (error: any) {
console.log(
`${colors.FgRed}Error:${colors.Reset} killing child processes => ${error.message}`
);
process.exit();
}
}
});
} catch (error: any) {
console.log(
`${colors.FgRed}Error:${colors.Reset} First run failed! => ${error.message}`
);
}
}