commit 05f380f691f158da4b7aaba5eced3ac84ee83056 Author: Benjamin Toby Date: Sun Oct 29 08:35:26 2023 +0100 First Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f721fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +/test \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..64e8f08 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# Simple CI/CD package for any application + +Integrate a simple CI/CD process into your application without the hassle. + +_**NOTE:** This package needs `node` installed to work_ + +## Requirements + +- **Node JS Runtime and NPM:** You need to have `NodeJS` and `npm` installed on the target machine for this package to work. +- **`nodecid.config.json` file:** This package depends on a configuration file located in the root directory of your application. + +## Installation + +To install this package globally just run: + +```shell +npm install -g nodecid +``` + +To run the package directly run: + +```shell +npx nodecid +``` + +This will download the package and run the binaries directly. After the first run it won't download the package again. + +## Usage + +To run the package after installing it globally just run: + +```shell +nodecid +``` + +Remember you must have a `nodecid.config.json` file located in your root directory else this will throw an error. + +### Configuration + +Your `nodecid.config.json` file should look like this: + +```json +{ + "start": "node index.js", + "preflight": ["npm run test", "npm run build"] +} +``` + +or + +```json +{ + "start": "node index.js", + "preflight": "./preflight.sh" +} +``` + +Your `preflight` parameter can wither be an array of commands, or path to a shell script. This will run before every `start` commands. + +Optionally you could include a `redeploy_path` in your config file: + +```json +{ + "start": "node index.js", + "preflight": "./preflight.sh", + "redeploy_path": "./REDEPLOY" +} +``` + +This will look for the file named `REDEPLOY` in your rood directory and watch that file. If the file is changed the application will be restarted, ie it will run the `preflight` command(s) and `start` command. If you ommit the `redeploy_path` a file named `REDEPLOY` will be created in your root directory. + +You can change the name and path of the `redeploy_path`, just make sure the path is correct and the file name exists in the named path. Example: + +```json +{ + "start": "node index.js", + "preflight": "./preflight.sh", + "redeploy_path": "./deploy/trigger.txt" +} +``` + +_NOTE:_ This also works for other languages, example: + +```json +{ + "start": "python app.py", + "preflight": "./preflight.sh" +} +``` + +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. + +### 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. diff --git a/config/nodecid.schema.json b/config/nodecid.schema.json new file mode 100644 index 0000000..bfd93d3 --- /dev/null +++ b/config/nodecid.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "preflight": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "redeploy_path": { + "type": "string" + } + }, + "required": ["start", "preflight"] +} diff --git a/deploy/start.js b/deploy/start.js new file mode 100755 index 0000000..0fd790c --- /dev/null +++ b/deploy/start.js @@ -0,0 +1,143 @@ +// @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; diff --git a/index.js b/index.js new file mode 100644 index 0000000..3e4d43c --- /dev/null +++ b/index.js @@ -0,0 +1,56 @@ +#! /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}` + ); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b0d9cb9 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "nodecid", + "version": "1.0.0", + "description": "Simple CI/CD process", + "main": "index.js", + "bin": { + "nodecid": "./index.js", + "node-ci-cd": "./index.js" + }, + "keywords": [ + "CI/CD", + "Continuous Integration", + "Continous Deployment" + ], + "author": "Benjamin Toby", + "license": "MIT" +} diff --git a/types.d.js b/types.d.js new file mode 100644 index 0000000..1cd096f --- /dev/null +++ b/types.d.js @@ -0,0 +1,8 @@ +/** + * @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` + */ diff --git a/utils/console-colors.js b/utils/console-colors.js new file mode 100755 index 0000000..708acf6 --- /dev/null +++ b/utils/console-colors.js @@ -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", +}; + +module.exports = colors; diff --git a/utils/triggers/github.js b/utils/triggers/github.js new file mode 100644 index 0000000..59711ff --- /dev/null +++ b/utils/triggers/github.js @@ -0,0 +1,3 @@ +function githubWebhook() { + return true; +}