From ec9347f8c60811cb02d14f028b78f188c3dc18ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dragan=20Filipovi=C4=87?= Date: Mon, 2 Jan 2023 21:06:33 +0100 Subject: [PATCH] Feature/ssh cmd (#94) * feat: Add SSH remote script support - before and after rsync * fix: remove __dirname * feat: add sshCmdArgs option * Add promise instead of callback * fix: improve logs * fix: Add simple command exists instead of a plugin * add non interactive install * feat: add onStderr and onStdout logs * Improve reject messages * feat: Add RSYNC_STDOUT env variable * emoji updates * fix: update workflow actions --- .eslintrc.js | 12 +-- .github/workflows/build.yml | 4 +- .github/workflows/codeql-analysis.yml | 6 +- .github/workflows/e2e.yml | 11 ++- .github/workflows/manual-release.yml | 8 +- README.md | 29 ++++++- action.yml | 22 +++-- dist/index.js | 2 +- package-lock.json | 65 +++++++-------- package.json | 1 - src/helpers.js | 85 +++++++++++++------ src/index.js | 116 ++++++++++---------------- src/inputs.js | 43 +++++++++- src/remoteCmd.js | 40 +++++++++ src/rsyncCli.js | 100 ++++++++++++++-------- src/sshKey.js | 58 +++++++------ src/test.js | 13 --- 17 files changed, 373 insertions(+), 242 deletions(-) create mode 100644 src/remoteCmd.js delete mode 100644 src/test.js diff --git a/.eslintrc.js b/.eslintrc.js index 1324c84..df1d632 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,14 +12,14 @@ module.exports = { SharedArrayBuffer: 'readonly' }, parserOptions: { - ecmaVersion: 2018, + ecmaVersion: 2018 }, rules: { - "comma-dangle": [ - "error", - "never" + 'comma-dangle': [ + 'error', + 'never' ], - "no-console": "off", - "object-curly-newline": "off" + 'no-console': 'off', + 'object-curly-newline': 'off' } }; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b8389e..e7e594e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,9 +18,9 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Install dependencies diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ffd76f4..781156d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -31,11 +31,11 @@ jobs: language: [ 'javascript' ] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} @@ -44,4 +44,4 @@ jobs: npm run build --if-present - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 07c07f0..74fd9a5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -2,7 +2,7 @@ name: e2e Test on: push: - branches: [ 'main' ] + branches: [ 'feature/ssh-cmd' ] env: TEST_HOST_DOCKER: ./test @@ -55,7 +55,7 @@ jobs: cat index.html - name: e2e Test published ssh-deploy action - uses: easingthemes/ssh-deploy@main + uses: easingthemes/ssh-deploy@feature/ssh-cmd env: # SSH_PRIVATE_KEY: $EXAMPLE_SSH_PRIVATE_KEY # REMOTE_HOST: $EXAMPLE_REMOTE_HOST1 @@ -64,3 +64,10 @@ jobs: SOURCE: "test_project/" TARGET: "/var/www/html/" EXCLUDE: "/dist/, /node_modules/" + SCRIPT_BEFORE: | + whoami + ls -al + SCRIPT_AFTER: | + whoami + ls -al + echo $RSYNC_STDOUT diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml index 43b48d3..e4040ad 100644 --- a/.github/workflows/manual-release.yml +++ b/.github/workflows/manual-release.yml @@ -16,9 +16,9 @@ jobs: node-version: [ 16.x ] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix['node-version'] }} - name: Install dependencies @@ -28,11 +28,11 @@ jobs: - name: Run Tests run: npm test --if-present - name: Release - uses: cycjimmy/semantic-release-action@v2 + uses: cycjimmy/semantic-release-action@v3 with: dry_run: ${{ github.event.inputs.dryrun == 'true' }} extra_plugins: | - @semantic-release/changelog@3.0.0 + @semantic-release/changelog @semantic-release/git env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index fdc7162..9c88b2e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # ssh deployments -Deploy code with rsync over ssh, using NodeJS. +Deploy code with rsync over ssh. + +Execute remote scripts before or after rsync NodeJS version is more than a minute `faster` than simple Docker version. @@ -8,6 +10,8 @@ This GitHub Action deploys specific directory from `GITHUB_WORKSPACE` to a folde This action would usually follow a build/test action which leaves deployable code in `GITHUB_WORKSPACE`, eg `dist`; +In addition to rsync, this action provides scripts execution on remote host before and/or after rsync. + # Configuration Pass configuration with `env` vars @@ -53,6 +57,16 @@ The target directory path to exclude separated by `,`, ie: `/dist/, /node_modules/` +##### 9. `SCRIPT_BEFORE` (optional, default '') + +Script to run on host machine before rsync. Single line or multiline commands. +Execution is preformed by storing commands in `.sh` file and executing it via `.bash` over `ssh` + +##### 10. `SCRIPT_AFTER` (optional, default '') + +Script to run on host machine after rsync. +Rsync output is stored in `$RSYNC_STDOUT` env variable. + # Usage Use the latest version from Marketplace,eg: ssh-deploy@v2 @@ -69,6 +83,13 @@ or use the latest version from a branch, eg: ssh-deploy@main REMOTE_USER: ${{ secrets.REMOTE_USER }} TARGET: ${{ secrets.REMOTE_TARGET }} EXCLUDE: "/dist/, /node_modules/" + SCRIPT_BEFORE: | + whoami + ls -al + SCRIPT_AFTER: | + whoami + ls -al + echo $RSYNC_STDOUT ``` # Example usage in workflow @@ -107,13 +128,13 @@ jobs: ## Issues -This is a Github Action wrapping `rsync` via `ssh`. Only issues with action functionality can be fixed here. +This is a GitHub Action wrapping `rsync` via `ssh`. Only issues with action functionality can be fixed here. Almost 95% of the issues are related to wrong SSH connection or `rsync` params and permissions. -This issues are not related to the action itself. +These issues are not related to the action itself. - Check manually your ssh connection from your client before opening a bug report. -- Check `rsync` params for your usecase. Default params are not going to be enough wor everyone, it highly depends on your setup. +- Check `rsync` params for your use-case. Default params are not going to be enough wor everyone, it highly depends on your setup. - Check manually your rsync command from your client before opening a bug report. I've added e2e test for this action. diff --git a/action.yml b/action.yml index 15aa86e..f26e459 100644 --- a/action.yml +++ b/action.yml @@ -1,9 +1,9 @@ name: "ssh deploy" -description: "NodeJS action for FAST deployment with rsync/ssh" +description: "NodeJS action for FAST deployment with rsync/ssh and remote script execution before/after rsync" author: "easingthemes" inputs: - SSH_PRIVATE_KEY: # Private Key - description: "Private Key" + SSH_PRIVATE_KEY: + description: "Private key part of an SSH key pair" required: true REMOTE_HOST: description: "Remote host" @@ -16,7 +16,7 @@ inputs: required: false default: "22" SOURCE: - description: "Source directory" + description: "Source directory, path relative to `$GITHUB_WORKSPACE` root, eg: `dist/`" required: false default: "" TARGET: @@ -27,8 +27,20 @@ inputs: description: "Arguments to pass to rsync" required: false default: "-rltgoDzvO" + SSH_CMD_ARGS: + description: "An array of ssh arguments, they must be prefixed with -o and separated by a comma, for example: -o SomeArgument=no, -o SomeOtherArgument=5 " + required: false + default: "-o StrictHostKeyChecking=no" EXCLUDE: - description: "An array of folder to exclude" + description: "paths to exclude separated by `,`, ie: `/dist/, /node_modules/`" + required: false + default: "" + SCRIPT_BEFORE: + description: "Script to run on host machine before rsync" + required: false + default: "" + SCRIPT_AFTER: + description: "Script to run on host machine after rsync" required: false default: "" outputs: diff --git a/dist/index.js b/dist/index.js index 3d77b41..c4f5732 100755 --- a/dist/index.js +++ b/dist/index.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -(()=>{var e={569:(e,r,s)=>{e.exports=s(325)},325:(e,r,s)=>{"use strict";var t=s(81).exec;var n=s(81).execSync;var o=s(147);var c=s(17);var i=o.access;var a=o.accessSync;var u=o.constants||o;var l=process.platform=="win32";var fileNotExists=function(e,r){i(e,u.F_OK,(function(e){r(!e)}))};var fileNotExistsSync=function(e){try{a(e,u.F_OK);return false}catch(e){return true}};var localExecutable=function(e,r){i(e,u.F_OK|u.X_OK,(function(e){r(null,!e)}))};var localExecutableSync=function(e){try{a(e,u.F_OK|u.X_OK);return true}catch(e){return false}};var commandExistsUnix=function(e,r,s){fileNotExists(e,(function(n){if(!n){var o=t("command -v "+r+" 2>/dev/null"+" && { echo >&1 "+r+"; exit 0; }",(function(e,r,t){s(null,!!r)}));return}localExecutable(e,s)}))};var commandExistsWindows=function(e,r,s){if(!/^(?!(?:.*\s|.*\.|\W+)$)(?:[a-zA-Z]:)?(?:(?:[^<>:"\|\?\*\n])+(?:\/\/|\/|\\\\|\\)?)+$/m.test(e)){s(null,false);return}var n=t("where "+r,(function(e){if(e!==null){s(null,false)}else{s(null,true)}}))};var commandExistsUnixSync=function(e,r){if(fileNotExistsSync(e)){try{var s=n("command -v "+r+" 2>/dev/null"+" && { echo >&1 "+r+"; exit 0; }");return!!s}catch(e){return false}}return localExecutableSync(e)};var commandExistsWindowsSync=function(e,r,s){if(!/^(?!(?:.*\s|.*\.|\W+)$)(?:[a-zA-Z]:)?(?:(?:[^<>:"\|\?\*\n])+(?:\/\/|\/|\\\\|\\)?)+$/m.test(e)){return false}try{var t=n("where "+r,{stdio:[]});return!!t}catch(e){return false}};var cleanInput=function(e){if(/[^A-Za-z0-9_\/:=-]/.test(e)){e="'"+e.replace(/'/g,"'\\''")+"'";e=e.replace(/^(?:'')+/g,"").replace(/\\'''/g,"\\'")}return e};if(l){cleanInput=function(e){var r=/[\\]/.test(e);if(r){var s='"'+c.dirname(e)+'"';var t='"'+c.basename(e)+'"';return s+":"+t}return'"'+e+'"'}}e.exports=function commandExists(e,r){var s=cleanInput(e);if(!r&&typeof Promise!=="undefined"){return new Promise((function(r,s){commandExists(e,(function(t,n){if(n){r(e)}else{s(t)}}))}))}if(l){commandExistsWindows(e,s,r)}else{commandExistsUnix(e,s,r)}};e.exports.sync=function(e){var r=cleanInput(e);if(l){return commandExistsWindowsSync(e,r)}else{return commandExistsUnixSync(e,r)}}},898:(e,r,s)=>{"use strict";var t=s(81).spawn;var n=s(837);var escapeSpaces=function(e){if(typeof e==="string"){return e.replace(/\b\s/g,"\\ ")}else{return e}};var escapeSpacesInOptions=function(e){["src","dest","include","exclude","excludeFirst"].forEach((function(r){var s=e[r];if(typeof s==="string"){e[r]=escapeSpaces(s)}else if(Array.isArray(s)===true){e[r]=s.map(escapeSpaces)}}));return e};e.exports=function(e,r){e=e||{};e=n._extend({},e);e=escapeSpacesInOptions(e);var s=e.platform||process.platform;var o=s==="win32";if(typeof e.src==="undefined"){throw new Error("'src' directory is missing from options")}if(typeof e.dest==="undefined"){throw new Error("'dest' directory is missing from options")}var c=e.dest;if(typeof e.host!=="undefined"){c=e.host+":"+e.dest}if(!Array.isArray(e.src)){e.src=[e.src]}var i=[].concat(e.src);i.push(c);var a=(e.args||[]).find((function(e){return e.match(/--chmod=/)}));if(o&&!a){i.push("--chmod=ugo=rwX")}if(typeof e.host!=="undefined"||e.ssh){i.push("--rsh");var u="ssh";if(typeof e.port!=="undefined"){u+=" -p "+e.port}if(typeof e.privateKey!=="undefined"){u+=" -i "+e.privateKey}if(typeof e.sshCmdArgs!=="undefined"){u+=" "+e.sshCmdArgs.join(" ")}i.push(u)}if(e.recursive===true){i.push("--recursive")}if(e.times===true){i.push("--times")}if(e.syncDest===true||e.deleteAll===true){i.push("--delete");i.push("--delete-excluded")}if(e.syncDestIgnoreExcl===true||e.delete===true){i.push("--delete")}if(e.dryRun===true){i.push("--dry-run");i.push("--verbose")}if(typeof e.excludeFirst!=="undefined"&&n.isArray(e.excludeFirst)){e.excludeFirst.forEach((function(e,r){i.push("--exclude="+e)}))}if(typeof e.include!=="undefined"&&n.isArray(e.include)){e.include.forEach((function(e,r){i.push("--include="+e)}))}if(typeof e.exclude!=="undefined"&&n.isArray(e.exclude)){e.exclude.forEach((function(e,r){i.push("--exclude="+e)}))}switch(e.compareMode){case"sizeOnly":i.push("--size-only");break;case"checksum":i.push("--checksum");break}if(typeof e.args!=="undefined"&&n.isArray(e.args)){i=[...new Set([...i,...e.args])]}i=[...new Set(i)];var noop=function(){};var l=e.onStdout||noop;var d=e.onStderr||noop;var f="rsync ";i.forEach((function(e){if(e.substr(0,4)==="ssh "){e='"'+e+'"'}f+=e+" "}));f=f.trim();if(e.noExec){r(null,null,null,f);return}try{var p="";var y="";var v;if(o){v=t("cmd.exe",["/s","/c",'"'+f+'"'],{windowsVerbatimArguments:true,stdio:[process.stdin,"pipe","pipe"]})}else{v=t("/bin/sh",["-c",f])}v.stdout.on("data",(function(e){l(e);p+=e}));v.stderr.on("data",(function(e){d(e);y+=e}));v.on("exit",(function(e){var s=null;if(e!==0){s=new Error("rsync exited with code "+e);s.code=e}r(s,p,y,f)}))}catch(e){r(e,null,null,f)}}},505:(e,r,s)=>{const{existsSync:t,mkdirSync:n,writeFileSync:o}=s(147);const{GITHUB_WORKSPACE:c}=process.env;const validateDir=e=>{if(!t(e)){console.log(`[SSH] Creating ${e} dir in `,c);n(e);console.log("✅ [SSH] dir created.")}else{console.log(`[SSH] ${e} dir exist`)}};const validateFile=e=>{if(!t(e)){console.log(`[SSH] Creating ${e} file in `,c);try{o(e,"",{encoding:"utf8",mode:384});console.log("✅ [SSH] file created.")}catch(r){console.error("⚠️ [SSH] writeFileSync error",e,r.message);process.abort()}}else{console.log(`[SSH] ${e} file exist`)}};e.exports={validateDir:validateDir,validateFile:validateFile}},229:e=>{const r=["REMOTE_HOST","REMOTE_USER","REMOTE_PORT","SSH_PRIVATE_KEY","DEPLOY_KEY_NAME","SOURCE","TARGET","ARGS","EXCLUDE"];const s={GITHUB_WORKSPACE:process.env.GITHUB_WORKSPACE};r.forEach((e=>{s[e]=process.env[e]||process.env[`INPUT_${e}`]}));e.exports=s},447:(e,r,s)=>{const{sync:t}=s(569);const{exec:n,execSync:o}=s(81);const validateRsync=(e=(()=>{}))=>{const r=t("rsync");if(r){console.log("⚠️ [CLI] Rsync exists");const r=o("rsync --version",{stdio:"inherit"});return e()}console.log('⚠️ [CLI] Rsync doesn\'t exists. Start installation with "apt-get" \n');n("sudo apt-get update && sudo apt-get --no-install-recommends install rsync",((r,s,t)=>{if(r){console.log("⚠️ [CLI] Rsync installation failed. Aborting ... ",r.message);process.abort()}else{console.log("✅ [CLI] Rsync installed. \n",s,t);e()}}))};const validateInputs=e=>{const r=Object.keys(e);const s=r.filter((r=>{const s=e[r];if(!s){console.error(`⚠️ [INPUTS] ${r} is mandatory`)}return s}));if(s.length!==r.length){console.error("⚠️ [INPUTS] Inputs not valid, aborting ...");process.abort()}};e.exports={validateRsync:validateRsync,validateInputs:validateInputs}},822:(e,r,s)=>{const{writeFileSync:t}=s(147);const{join:n}=s(17);const{validateDir:o,validateFile:c}=s(505);const{HOME:i}=process.env;const addSshKey=(e,r)=>{const s=n(i||__dirname,".ssh");const a=n(s,r);o(s);c(`${s}/known_hosts`);try{t(a,e,{encoding:"utf8",mode:384})}catch(e){console.error("⚠️ writeFileSync error",a,e.message);process.abort()}console.log("✅ Ssh key added to `.ssh` dir ",a);return a};e.exports={addSshKey:addSshKey}},81:e=>{"use strict";e.exports=require("child_process")},147:e=>{"use strict";e.exports=require("fs")},17:e=>{"use strict";e.exports=require("path")},837:e=>{"use strict";e.exports=require("util")}};var r={};function __nccwpck_require__(s){var t=r[s];if(t!==undefined){return t.exports}var n=r[s]={exports:{}};var o=true;try{e[s](n,n.exports,__nccwpck_require__);o=false}finally{if(o)delete r[s]}return n.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var s={};(()=>{const e=__nccwpck_require__(898);const{validateRsync:r,validateInputs:s}=__nccwpck_require__(447);const{addSshKey:t}=__nccwpck_require__(822);const{REMOTE_HOST:n,REMOTE_USER:o,REMOTE_PORT:c,SSH_PRIVATE_KEY:i,DEPLOY_KEY_NAME:a,SOURCE:u,TARGET:l,ARGS:d,EXCLUDE:f,GITHUB_WORKSPACE:p}=__nccwpck_require__(229);const y={ssh:true,sshCmdArgs:["-o StrictHostKeyChecking=no"],recursive:true};console.log("GITHUB_WORKSPACE: ",p);console.log("REMOTE_HOST: ",process.env.REMOTE_HOST);console.log("REMOTE_USER: ",process.env.REMOTE_USER);const v=(()=>{const rsync=({privateKey:r,port:s,src:t,dest:n,args:o,exclude:c})=>{console.log(`[Rsync] Starting Rsync Action: ${t} to ${n}`);if(c)console.log(`[Rsync] exluding folders ${c}`);try{e({src:t,dest:n,args:o,privateKey:r,port:s,excludeFirst:c,...y},((e,r,s,t)=>{if(e){console.error("⚠️ [Rsync] error: ",e.message);console.log("⚠️ [Rsync] stderr: ",s);console.log("⚠️ [Rsync] stdout: ",r);console.log("⚠️ [Rsync] cmd: ",t);process.abort()}else{console.log("✅ [Rsync] finished.",r)}}))}catch(e){console.error("⚠️ [Rsync] command error: ",e.message,e.stack);process.abort()}};const init=({src:e,dest:s,args:n,host:o="localhost",port:c,username:i,privateKeyContent:u,exclude:l=[]})=>{r((()=>{const r=t(u,a||"deploy_key");const d=`${i}@${o}:${s}`;rsync({privateKey:r,port:c,src:e,dest:d,args:n,exclude:l})}))};return{init:init}})();const run=()=>{s({SSH_PRIVATE_KEY:i,REMOTE_HOST:n,REMOTE_USER:o});v.init({src:`${p}/${u||""}`,dest:l||`/home/${o}/`,args:d?[d]:["-rltgoDzvO"],host:n,port:c||"22",username:o,privateKeyContent:i,exclude:(f||"").split(",").map((e=>e.trim()))})};run()})();module.exports=s})(); \ No newline at end of file +(()=>{var e={898:(e,s,r)=>{"use strict";var t=r(81).spawn;var o=r(837);var escapeSpaces=function(e){if(typeof e==="string"){return e.replace(/\b\s/g,"\\ ")}else{return e}};var escapeSpacesInOptions=function(e){["src","dest","include","exclude","excludeFirst"].forEach((function(s){var r=e[s];if(typeof r==="string"){e[s]=escapeSpaces(r)}else if(Array.isArray(r)===true){e[s]=r.map(escapeSpaces)}}));return e};e.exports=function(e,s){e=e||{};e=o._extend({},e);e=escapeSpacesInOptions(e);var r=e.platform||process.platform;var n=r==="win32";if(typeof e.src==="undefined"){throw new Error("'src' directory is missing from options")}if(typeof e.dest==="undefined"){throw new Error("'dest' directory is missing from options")}var c=e.dest;if(typeof e.host!=="undefined"){c=e.host+":"+e.dest}if(!Array.isArray(e.src)){e.src=[e.src]}var i=[].concat(e.src);i.push(c);var a=(e.args||[]).find((function(e){return e.match(/--chmod=/)}));if(n&&!a){i.push("--chmod=ugo=rwX")}if(typeof e.host!=="undefined"||e.ssh){i.push("--rsh");var d="ssh";if(typeof e.port!=="undefined"){d+=" -p "+e.port}if(typeof e.privateKey!=="undefined"){d+=" -i "+e.privateKey}if(typeof e.sshCmdArgs!=="undefined"){d+=" "+e.sshCmdArgs.join(" ")}i.push(d)}if(e.recursive===true){i.push("--recursive")}if(e.times===true){i.push("--times")}if(e.syncDest===true||e.deleteAll===true){i.push("--delete");i.push("--delete-excluded")}if(e.syncDestIgnoreExcl===true||e.delete===true){i.push("--delete")}if(e.dryRun===true){i.push("--dry-run");i.push("--verbose")}if(typeof e.excludeFirst!=="undefined"&&o.isArray(e.excludeFirst)){e.excludeFirst.forEach((function(e,s){i.push("--exclude="+e)}))}if(typeof e.include!=="undefined"&&o.isArray(e.include)){e.include.forEach((function(e,s){i.push("--include="+e)}))}if(typeof e.exclude!=="undefined"&&o.isArray(e.exclude)){e.exclude.forEach((function(e,s){i.push("--exclude="+e)}))}switch(e.compareMode){case"sizeOnly":i.push("--size-only");break;case"checksum":i.push("--checksum");break}if(typeof e.args!=="undefined"&&o.isArray(e.args)){i=[...new Set([...i,...e.args])]}i=[...new Set(i)];var noop=function(){};var l=e.onStdout||noop;var u=e.onStderr||noop;var f="rsync ";i.forEach((function(e){if(e.substr(0,4)==="ssh "){e='"'+e+'"'}f+=e+" "}));f=f.trim();if(e.noExec){s(null,null,null,f);return}try{var p="";var h="";var y;if(n){y=t("cmd.exe",["/s","/c",'"'+f+'"'],{windowsVerbatimArguments:true,stdio:[process.stdin,"pipe","pipe"]})}else{y=t("/bin/sh",["-c",f])}y.stdout.on("data",(function(e){l(e);p+=e}));y.stderr.on("data",(function(e){u(e);h+=e}));y.on("exit",(function(e){var r=null;if(e!==0){r=new Error("rsync exited with code "+e);r.code=e}s(r,p,h,f)}))}catch(e){s(e,null,null,f)}}},505:(e,s,r)=>{const{existsSync:t,mkdirSync:o,writeFileSync:n}=r(147);const{join:c}=r(17);const validateDir=e=>{if(!e){console.warn("⚠️ [DIR] dir is not defined");return}if(t(e)){console.log(`✅ [DIR] ${e} dir exist`);return}console.log(`[DIR] Creating ${e} dir in workspace root`);o(e);console.log("✅ [DIR] dir created.")};const handleError=(e,s)=>{if(s){throw new Error(e)}console.warn(e)};const writeToFile=({dir:e,filename:s,content:r,isRequired:o,mode:i="0644"})=>{validateDir(e);const a=c(e,s);if(t(a)){const e=`⚠️ [FILE] ${a} Required file exist.`;handleError(e,o);return}try{console.log(`[FILE] writing ${a} file ...`,r.length);n(a,r,{encoding:"utf8",mode:i})}catch(e){const s=`⚠️[FILE] Writing to file error. filePath: ${a}, message: ${e.message}`;handleError(s,o)}};const validateRequiredInputs=e=>{const s=Object.keys(e);const r=s.filter((s=>{const r=e[s];if(!r){console.error(`❌ [INPUTS] ${s} is mandatory`)}return r}));if(r.length!==s.length){throw new Error("⚠️ [INPUTS] Inputs not valid, aborting ...")}};const snakeToCamel=e=>e.replace(/[^a-zA-Z0-9]+(.)/g,((e,s)=>s.toUpperCase()));e.exports={writeToFile:writeToFile,validateRequiredInputs:validateRequiredInputs,snakeToCamel:snakeToCamel}},229:(e,s,r)=>{const{snakeToCamel:t}=r(505);const o=["REMOTE_HOST","REMOTE_USER","REMOTE_PORT","SSH_PRIVATE_KEY","DEPLOY_KEY_NAME","SOURCE","TARGET","ARGS","SSH_CMD_ARGS","EXCLUDE","SCRIPT_BEFORE","SCRIPT_AFTER"];const n=process.env.GITHUB_WORKSPACE;const c=process.env.REMOTE_USER;const i={source:"",target:`/home/${c}/`,exclude:"",args:"-rltgoDzvO",sshCmdArgs:"-o StrictHostKeyChecking=no",deployKeyName:"deploy_key"};const a={githubWorkspace:n};o.forEach((e=>{const s=t(e.toLowerCase());const r=process.env[e]||process.env[`INPUT_${e}`];const o=r===undefined?i[s]:r;let c=o;switch(s){case"source":c=`${n}/${o}`;break;case"exclude":case"args":case"sshCmdArgs":c=o.split(",").map((e=>e.trim()));break}a[s]=c}));a.sshServer=`${a.remoteUser}@${a.remoteHost}`;a.rsyncServer=`${a.remoteUser}@${a.remoteHost}:${a.target}`;e.exports=a},976:(e,s,r)=>{const{exec:t}=r(81);const{sshServer:o,githubWorkspace:n}=r(229);const{writeToFile:c}=r(505);const handleError=(e,s,r)=>{if(s){r(new Error(e))}else{console.warn(e)}};const remoteCmd=async(e,s,r,i)=>new Promise(((a,d)=>{const l=`local_ssh_script-${i}.sh`;try{c({dir:n,filename:l,content:e});console.log(`Executing remote script: ssh -i ${s} ${o}`);t(`DEBIAN_FRONTEND=noninteractive ssh -i ${s} ${o} 'RSYNC_STDOUT="${process.env.RSYNC_STDOUT}" bash -s' < ${l}`,((e,s,t)=>{if(e){const o=`⚠️ [CMD] Remote script failed: ${e.message}`;console.warn(`${o} \n`,s,t);handleError(o,r,d)}else{console.log("✅ [CMD] Remote script executed. \n",s,t);a(s)}}))}catch(e){handleError(e.message,r,d)}}));e.exports={remoteCmdBefore:async(e,s,r)=>remoteCmd(e,s,r,"before"),remoteCmdAfter:async(e,s,r)=>remoteCmd(e,s,r,"after")}},447:(e,s,r)=>{const{execSync:t}=r(81);const o=r(898);const nodeRsyncPromise=async e=>new Promise(((s,r)=>{try{o(e,((e,t,o,n)=>{if(e){console.error("❌ [Rsync] error: ");console.error(e);console.error("❌ [Rsync] stderr: ");console.error(o);console.error("❌️ [Rsync] stdout: ");console.error(t);console.error("❌ [Rsync] cmd: ",n);r(new Error(`${e.message}\n\n${o}`))}else{s(t)}}))}catch(e){console.error("❌ [Rsync] command error: ",e.message,e.stack);r(e)}}));const validateRsync=async()=>{try{t("rsync --version",{stdio:"inherit"});console.log("✅️ [CLI] Rsync exists");return}catch(e){console.warn("⚠️ [CLI] Rsync doesn't exists",e.message)}console.log('[CLI] Start rsync installation with "apt-get" \n');try{t("sudo DEBIAN_FRONTEND=noninteractive apt-get -y update && sudo DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends -y install rsync",{stdio:"inherit"});console.log("✅ [CLI] Rsync installed. \n")}catch(e){throw new Error(`⚠️ [CLI] Rsync installation failed. Aborting ... error: ${e.message}`)}};const rsyncCli=async({source:e,rsyncServer:s,exclude:r,remotePort:t,privateKeyPath:o,args:n,sshCmdArgs:c})=>{console.log(`[Rsync] Starting Rsync Action: ${e} to ${s}`);if(r)console.log(`[Rsync] excluding folders ${r}`);const i={ssh:true,recursive:true};return nodeRsyncPromise({...i,src:e,dest:s,excludeFirst:r,port:t,privateKey:o,args:n,sshCmdArgs:c,onStdout:e=>console.log(e),onStderr:e=>console.error(e)})};const sshDeploy=async e=>{await validateRsync();const s=await rsyncCli(e);console.log("✅ [Rsync] finished.",s);process.env.RSYNC_STDOUT=`${s}`;return s};e.exports={sshDeploy:sshDeploy}},822:(e,s,r)=>{const{join:t}=r(17);const{execSync:o}=r(81);const{writeToFile:n}=r(505);const c="known_hosts";const getPrivateKeyPath=(e="")=>{const{HOME:s}=process.env;const r=t(s||__dirname,".ssh");const o=t(r,c);return{dir:r,filename:e,path:t(r,e),knownHostsPath:o}};const addSshKey=(e,s)=>{const{dir:r,filename:t}=getPrivateKeyPath(s);n({dir:r,filename:c,content:""});console.log("✅ [SSH] known_hosts file ensured",r);n({dir:r,filename:t,content:e,isRequired:true,mode:"0400"});console.log("✅ [SSH] key added to `.ssh` dir ",r,t)};const updateKnownHosts=e=>{const{knownHostsPath:s}=getPrivateKeyPath();console.log("[SSH] Adding host to `known_hosts` ....",e,s);try{o(`ssh-keyscan -H ${e} >> ${s}`,{stdio:"inherit"})}catch(s){console.error("❌ [SSH] Adding host to `known_hosts` ERROR",e,s.message)}console.log("✅ [SSH] Adding host to `known_hosts` DONE",e,s)};e.exports={getPrivateKeyPath:getPrivateKeyPath,updateKnownHosts:updateKnownHosts,addSshKey:addSshKey}},81:e=>{"use strict";e.exports=require("child_process")},147:e=>{"use strict";e.exports=require("fs")},17:e=>{"use strict";e.exports=require("path")},837:e=>{"use strict";e.exports=require("util")}};var s={};function __nccwpck_require__(r){var t=s[r];if(t!==undefined){return t.exports}var o=s[r]={exports:{}};var n=true;try{e[r](o,o.exports,__nccwpck_require__);n=false}finally{if(n)delete s[r]}return o.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var r={};(()=>{const{sshDeploy:e}=__nccwpck_require__(447);const{remoteCmdBefore:s,remoteCmdAfter:r}=__nccwpck_require__(976);const{addSshKey:t,getPrivateKeyPath:o,updateKnownHosts:n}=__nccwpck_require__(822);const{validateRequiredInputs:c}=__nccwpck_require__(505);const i=__nccwpck_require__(229);const run=async()=>{const{source:a,remoteUser:d,remoteHost:l,remotePort:u,deployKeyName:f,sshPrivateKey:p,args:h,exclude:y,sshCmdArgs:m,scriptBefore:g,scriptAfter:_,rsyncServer:w}=i;c({sshPrivateKey:p,remoteHost:l,remoteUser:d});t(p,f);const{path:v}=o(f);if(g||_){n(l)}if(g){await s(g,v)}await e({source:a,rsyncServer:w,exclude:y,remotePort:u,privateKeyPath:v,args:h,sshCmdArgs:m});if(_){await r(_,v)}};run().then(((e="")=>{console.log("✅ [DONE]",e)})).catch((e=>{console.error("❌ [ERROR]",e.message);process.exit(1)}))})();module.exports=r})(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4026c44..0c51f95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "@draganfilipovic/ssh-deploy", - "version": "3.0.1", + "version": "3.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@draganfilipovic/ssh-deploy", - "version": "3.0.1", + "version": "3.1.0", "license": "MIT", "dependencies": { - "command-exists": "^1.2.9", "rsyncwrapper": "^3.0.1" }, "devDependencies": { @@ -301,11 +300,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/command-exists": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -332,6 +326,21 @@ "node": ">= 8" } }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1922,21 +1931,6 @@ "punycode": "^2.1.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -2188,11 +2182,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "command-exists": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==" - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2214,6 +2203,17 @@ "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "debug": { @@ -3359,15 +3359,6 @@ "punycode": "^2.1.0" } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 756b0ee..702d5f3 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ }, "homepage": "https://github.com/easingthemes/ssh-deploy#readme", "dependencies": { - "command-exists": "^1.2.9", "rsyncwrapper": "^3.0.1" }, "devDependencies": { diff --git a/src/helpers.js b/src/helpers.js index e9c66d4..1c705df 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,38 +1,71 @@ const { existsSync, mkdirSync, writeFileSync } = require('fs'); - -const { - GITHUB_WORKSPACE -} = process.env; +const { join } = require('path'); const validateDir = (dir) => { - if (!existsSync(dir)) { - console.log(`[SSH] Creating ${dir} dir in `, GITHUB_WORKSPACE); - mkdirSync(dir); - console.log('✅ [SSH] dir created.'); - } else { - console.log(`[SSH] ${dir} dir exist`); + if (!dir) { + console.warn('⚠️ [DIR] dir is not defined'); + return; + } + if (existsSync(dir)) { + console.log(`✅ [DIR] ${dir} dir exist`); + return; + } + + console.log(`[DIR] Creating ${dir} dir in workspace root`); + mkdirSync(dir); + console.log('✅ [DIR] dir created.'); +}; + +const handleError = (message, isRequired) => { + if (isRequired) { + throw new Error(message); + } + console.warn(message); +}; + +const writeToFile = ({ dir, filename, content, isRequired, mode = '0644' }) => { + validateDir(dir); + const filePath = join(dir, filename); + + if (existsSync(filePath)) { + const message = `⚠️ [FILE] ${filePath} Required file exist.`; + handleError(message, isRequired); + return; + } + + try { + console.log(`[FILE] writing ${filePath} file ...`, content.length); + writeFileSync(filePath, content, { + encoding: 'utf8', + mode + }); + } catch (error) { + const message = `⚠️[FILE] Writing to file error. filePath: ${filePath}, message: ${error.message}`; + handleError(message, isRequired); } }; -const validateFile = (filePath) => { - if (!existsSync(filePath)) { - console.log(`[SSH] Creating ${filePath} file in `, GITHUB_WORKSPACE); - try { - writeFileSync(filePath, '', { - encoding: 'utf8', - mode: 0o600 - }); - console.log('✅ [SSH] file created.'); - } catch (e) { - console.error('⚠️ [SSH] writeFileSync error', filePath, e.message); - process.abort(); +const validateRequiredInputs = (inputs) => { + const inputKeys = Object.keys(inputs); + const validInputs = inputKeys.filter((inputKey) => { + const inputValue = inputs[inputKey]; + + if (!inputValue) { + console.error(`❌ [INPUTS] ${inputKey} is mandatory`); } - } else { - console.log(`[SSH] ${filePath} file exist`); + + return inputValue; + }); + + if (validInputs.length !== inputKeys.length) { + throw new Error('⚠️ [INPUTS] Inputs not valid, aborting ...'); } }; +const snakeToCamel = (str) => str.replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase()); + module.exports = { - validateDir, - validateFile + writeToFile, + validateRequiredInputs, + snakeToCamel }; diff --git a/src/index.js b/src/index.js index 8276749..0586e40 100644 --- a/src/index.js +++ b/src/index.js @@ -1,79 +1,47 @@ #!/usr/bin/env node -const nodeRsync = require('rsyncwrapper'); +const { sshDeploy } = require('./rsyncCli'); +const { remoteCmdBefore, remoteCmdAfter } = require('./remoteCmd'); +const { addSshKey, getPrivateKeyPath, updateKnownHosts } = require('./sshKey'); +const { validateRequiredInputs } = require('./helpers'); +const inputs = require('./inputs'); -const { validateRsync, validateInputs } = require('./rsyncCli'); -const { addSshKey } = require('./sshKey'); - -const { - REMOTE_HOST, REMOTE_USER, - REMOTE_PORT, SSH_PRIVATE_KEY, DEPLOY_KEY_NAME, - SOURCE, TARGET, ARGS, EXCLUDE, - GITHUB_WORKSPACE -} = require('./inputs'); - -const defaultOptions = { - ssh: true, - sshCmdArgs: ['-o StrictHostKeyChecking=no'], - recursive: true -}; - -console.log('GITHUB_WORKSPACE: ', GITHUB_WORKSPACE); -console.log('REMOTE_HOST: ', process.env.REMOTE_HOST); -console.log('REMOTE_USER: ', process.env.REMOTE_USER); - -const sshDeploy = (() => { - const rsync = ({ privateKey, port, src, dest, args, exclude }) => { - console.log(`[Rsync] Starting Rsync Action: ${src} to ${dest}`); - if (exclude) console.log(`[Rsync] exluding folders ${exclude}`); - - try { - // RSYNC COMMAND - nodeRsync({ - src, dest, args, privateKey, port, excludeFirst: exclude, ...defaultOptions - }, (error, stdout, stderr, cmd) => { - if (error) { - console.error('⚠️ [Rsync] error: ', error.message); - console.log('⚠️ [Rsync] stderr: ', stderr); - console.log('⚠️ [Rsync] stdout: ', stdout); - console.log('⚠️ [Rsync] cmd: ', cmd); - process.abort(); - } else { - console.log('✅ [Rsync] finished.', stdout); - } - }); - } catch (err) { - console.error('⚠️ [Rsync] command error: ', err.message, err.stack); - process.abort(); - } - }; - - const init = ({ src, dest, args, host = 'localhost', port, username, privateKeyContent, exclude = [] }) => { - validateRsync(() => { - const privateKey = addSshKey(privateKeyContent, DEPLOY_KEY_NAME || 'deploy_key'); - const remoteDest = `${username}@${host}:${dest}`; - - rsync({ privateKey, port, src, dest: remoteDest, args, exclude }); - }); - }; - - return { - init - }; -})(); - -const run = () => { - validateInputs({ SSH_PRIVATE_KEY, REMOTE_HOST, REMOTE_USER }); - - sshDeploy.init({ - src: `${GITHUB_WORKSPACE}/${SOURCE || ''}`, - dest: TARGET || `/home/${REMOTE_USER}/`, - args: ARGS ? [ARGS] : ['-rltgoDzvO'], - host: REMOTE_HOST, - port: REMOTE_PORT || '22', - username: REMOTE_USER, - privateKeyContent: SSH_PRIVATE_KEY, - exclude: (EXCLUDE || '').split(',').map((item) => item.trim()) // split by comma and trim whitespace +const run = async () => { + const { + source, remoteUser, remoteHost, remotePort, + deployKeyName, sshPrivateKey, + args, exclude, sshCmdArgs, + scriptBefore, scriptAfter, + rsyncServer + } = inputs; + // Validate required inputs + validateRequiredInputs({ sshPrivateKey, remoteHost, remoteUser }); + // Add SSH key + addSshKey(sshPrivateKey, deployKeyName); + const { path: privateKeyPath } = getPrivateKeyPath(deployKeyName); + // Update known hosts if ssh command is present to avoid prompt + if (scriptBefore || scriptAfter) { + updateKnownHosts(remoteHost); + } + // Check Script before + if (scriptBefore) { + await remoteCmdBefore(scriptBefore, privateKeyPath); + } + /* eslint-disable object-property-newline */ + await sshDeploy({ + source, rsyncServer, exclude, remotePort, + privateKeyPath, args, sshCmdArgs }); + // Check script after + if (scriptAfter) { + await remoteCmdAfter(scriptAfter, privateKeyPath); + } }; -run(); +run() + .then((data = '') => { + console.log('✅ [DONE]', data); + }) + .catch((error) => { + console.error('❌ [ERROR]', error.message); + process.exit(1); + }); diff --git a/src/inputs.js b/src/inputs.js index 9b7638e..f636080 100644 --- a/src/inputs.js +++ b/src/inputs.js @@ -1,11 +1,48 @@ -const inputNames = ['REMOTE_HOST', 'REMOTE_USER', 'REMOTE_PORT', 'SSH_PRIVATE_KEY', 'DEPLOY_KEY_NAME', 'SOURCE', 'TARGET', 'ARGS', 'EXCLUDE']; +const { snakeToCamel } = require('./helpers'); + +const inputNames = [ + 'REMOTE_HOST', 'REMOTE_USER', 'REMOTE_PORT', + 'SSH_PRIVATE_KEY', 'DEPLOY_KEY_NAME', + 'SOURCE', 'TARGET', 'ARGS', 'SSH_CMD_ARGS', 'EXCLUDE', + 'SCRIPT_BEFORE', 'SCRIPT_AFTER']; + +const githubWorkspace = process.env.GITHUB_WORKSPACE; +const remoteUser = process.env.REMOTE_USER; + +const defaultInputs = { + source: '', + target: `/home/${remoteUser}/`, + exclude: '', + args: '-rltgoDzvO', + sshCmdArgs: '-o StrictHostKeyChecking=no', + deployKeyName: 'deploy_key' +}; const inputs = { - GITHUB_WORKSPACE: process.env.GITHUB_WORKSPACE + githubWorkspace }; inputNames.forEach((input) => { - inputs[input] = process.env[input] || process.env[`INPUT_${input}`]; + const inputName = snakeToCamel(input.toLowerCase()); + const inputVal = process.env[input] || process.env[`INPUT_${input}`]; + const validVal = inputVal === undefined ? defaultInputs[inputName] : inputVal; + let extendedVal = validVal; + // eslint-disable-next-line default-case + switch (inputName) { + case 'source': + extendedVal = `${githubWorkspace}/${validVal}`; + break; + case 'exclude': + case 'args': + case 'sshCmdArgs': + extendedVal = validVal.split(',').map((item) => item.trim()); + break; + } + + inputs[inputName] = extendedVal; }); +inputs.sshServer = `${inputs.remoteUser}@${inputs.remoteHost}`; +inputs.rsyncServer = `${inputs.remoteUser}@${inputs.remoteHost}:${inputs.target}`; + module.exports = inputs; diff --git a/src/remoteCmd.js b/src/remoteCmd.js new file mode 100644 index 0000000..640234b --- /dev/null +++ b/src/remoteCmd.js @@ -0,0 +1,40 @@ +const { exec } = require('child_process'); +const { sshServer, githubWorkspace } = require('./inputs'); +const { writeToFile } = require('./helpers'); + +const handleError = (message, isRequired, callback) => { + if (isRequired) { + callback(new Error(message)); + } else { + console.warn(message); + } +}; + +// eslint-disable-next-line max-len +const remoteCmd = async (content, privateKeyPath, isRequired, label) => new Promise((resolve, reject) => { + const filename = `local_ssh_script-${label}.sh`; + try { + writeToFile({ dir: githubWorkspace, filename, content }); + console.log(`Executing remote script: ssh -i ${privateKeyPath} ${sshServer}`); + exec( + `DEBIAN_FRONTEND=noninteractive ssh -i ${privateKeyPath} ${sshServer} 'RSYNC_STDOUT="${process.env.RSYNC_STDOUT}" bash -s' < ${filename}`, + (err, data, stderr) => { + if (err) { + const message = `⚠️ [CMD] Remote script failed: ${err.message}`; + console.warn(`${message} \n`, data, stderr); + handleError(message, isRequired, reject); + } else { + console.log('✅ [CMD] Remote script executed. \n', data, stderr); + resolve(data); + } + } + ); + } catch (err) { + handleError(err.message, isRequired, reject); + } +}); + +module.exports = { + remoteCmdBefore: async (cmd, privateKeyPath, isRequired) => remoteCmd(cmd, privateKeyPath, isRequired, 'before'), + remoteCmdAfter: async (cmd, privateKeyPath, isRequired) => remoteCmd(cmd, privateKeyPath, isRequired, 'after') +}; diff --git a/src/rsyncCli.js b/src/rsyncCli.js index aa8190b..563bc50 100644 --- a/src/rsyncCli.js +++ b/src/rsyncCli.js @@ -1,46 +1,76 @@ -const { sync: commandExists } = require("command-exists"); -const { exec, execSync } = require("child_process"); +const { execSync } = require('child_process'); +const nodeRsync = require('rsyncwrapper'); -const validateRsync = (callback = () => {}) => { - const rsyncCli = commandExists("rsync"); - if (rsyncCli) { - console.log('⚠️ [CLI] Rsync exists'); - const rsyncVersion = execSync("rsync --version", { stdio: 'inherit' }); - return callback(); +const nodeRsyncPromise = async (config) => new Promise((resolve, reject) => { + try { + nodeRsync(config, (error, stdout, stderr, cmd) => { + if (error) { + console.error('❌ [Rsync] error: '); + console.error(error); + console.error('❌ [Rsync] stderr: '); + console.error(stderr); + console.error('❌️ [Rsync] stdout: '); + console.error(stdout); + console.error('❌ [Rsync] cmd: ', cmd); + reject(new Error(`${error.message}\n\n${stderr}`)); + } else { + resolve(stdout); + } + }); + } catch (error) { + console.error('❌ [Rsync] command error: ', error.message, error.stack); + reject(error); + } +}); + +const validateRsync = async () => { + try { + execSync('rsync --version', { stdio: 'inherit' }); + console.log('✅️ [CLI] Rsync exists'); + return; + } catch (error) { + console.warn('⚠️ [CLI] Rsync doesn\'t exists', error.message); } - console.log('⚠️ [CLI] Rsync doesn\'t exists. Start installation with "apt-get" \n'); + console.log('[CLI] Start rsync installation with "apt-get" \n'); + try { + execSync('sudo DEBIAN_FRONTEND=noninteractive apt-get -y update && sudo DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends -y install rsync', { stdio: 'inherit' }); + console.log('✅ [CLI] Rsync installed. \n'); + } catch (error) { + throw new Error(`⚠️ [CLI] Rsync installation failed. Aborting ... error: ${error.message}`); + } +}; - exec("sudo apt-get update && sudo apt-get --no-install-recommends install rsync", (err, data, stderr) => { - if (err) { - console.log("⚠️ [CLI] Rsync installation failed. Aborting ... ", err.message); - process.abort(); - } else { - console.log("✅ [CLI] Rsync installed. \n", data, stderr); - callback(); - } +const rsyncCli = async ({ + source, rsyncServer, exclude, remotePort, + privateKeyPath, args, sshCmdArgs +}) => { + console.log(`[Rsync] Starting Rsync Action: ${source} to ${rsyncServer}`); + if (exclude) console.log(`[Rsync] excluding folders ${exclude}`); + + const defaultOptions = { + ssh: true, + recursive: true + }; + + // RSYNC COMMAND + /* eslint-disable object-property-newline */ + return nodeRsyncPromise({ + ...defaultOptions, + src: source, dest: rsyncServer, excludeFirst: exclude, port: remotePort, + privateKey: privateKeyPath, args, sshCmdArgs, + onStdout: (data) => console.log(data), onStderr: (data) => console.error(data) }); }; -const validateInputs = (inputs) => { - const inputKeys = Object.keys(inputs); - const validInputs = inputKeys.filter((inputKey) => { - const inputValue = inputs[inputKey]; - - if (!inputValue) { - console.error(`⚠️ [INPUTS] ${inputKey} is mandatory`); - } - - return inputValue; - }); - - if (validInputs.length !== inputKeys.length) { - console.error("⚠️ [INPUTS] Inputs not valid, aborting ..."); - process.abort(); - } +const sshDeploy = async (params) => { + await validateRsync(); + const stdout = await rsyncCli(params); + console.log('✅ [Rsync] finished.', stdout); + process.env.RSYNC_STDOUT = `${stdout}`; + return stdout; }; module.exports = { - validateRsync, - validateInputs, + sshDeploy }; diff --git a/src/sshKey.js b/src/sshKey.js index 72eca6a..ce40d50 100644 --- a/src/sshKey.js +++ b/src/sshKey.js @@ -1,37 +1,43 @@ -const { writeFileSync } = require('fs'); const { join } = require('path'); +const { execSync } = require('child_process'); +const { writeToFile } = require('./helpers'); -const { - validateDir, - validateFile -} = require('./helpers'); +const KNOWN_HOSTS = 'known_hosts'; +const getPrivateKeyPath = (filename = '') => { + const { HOME } = process.env; + const dir = join(HOME || '~', '.ssh'); + const knownHostsPath = join(dir, KNOWN_HOSTS); + return { + dir, + filename, + path: join(dir, filename), + knownHostsPath + }; +}; -const { - HOME -} = process.env; - -const addSshKey = (key, name) => { - const sshDir = join(HOME || __dirname, '.ssh'); - const filePath = join(sshDir, name); - - validateDir(sshDir); - validateFile(`${sshDir}/known_hosts`); +const addSshKey = (content, deployKeyName) => { + const { dir, filename } = getPrivateKeyPath(deployKeyName); + writeToFile({ dir, filename: KNOWN_HOSTS, content: '' }); + console.log('✅ [SSH] known_hosts file ensured', dir); + writeToFile({ dir, filename, content, isRequired: true, mode: '0400' }); + console.log('✅ [SSH] key added to `.ssh` dir ', dir, filename); +}; +const updateKnownHosts = (host) => { + const { knownHostsPath } = getPrivateKeyPath(); + console.log('[SSH] Adding host to `known_hosts` ....', host, knownHostsPath); try { - writeFileSync(filePath, key, { - encoding: 'utf8', - mode: 0o600 + execSync(`ssh-keyscan -H ${host} >> ${knownHostsPath}`, { + stdio: 'inherit' }); - } catch (e) { - console.error('⚠️ writeFileSync error', filePath, e.message); - process.abort(); + } catch (error) { + console.error('❌ [SSH] Adding host to `known_hosts` ERROR', host, error.message); } - - console.log('✅ Ssh key added to `.ssh` dir ', filePath); - - return filePath; + console.log('✅ [SSH] Adding host to `known_hosts` DONE', host, knownHostsPath); }; module.exports = { + getPrivateKeyPath, + updateKnownHosts, addSshKey -} +}; diff --git a/src/test.js b/src/test.js deleted file mode 100644 index 75ab1c1..0000000 --- a/src/test.js +++ /dev/null @@ -1,13 +0,0 @@ -console.log('||||||||||||||||||||||||||||||||||||||'); -console.log('EXAMPLE_REMOTE_HOST: ', process.env.EXAMPLE_REMOTE_HOST); -console.log('EXAMPLE_REMOTE_USER: ', process.env.EXAMPLE_REMOTE_USER); -console.log('EXAMPLE_SSH_PRIVATE_KEY: ', process.env.EXAMPLE_SSH_PRIVATE_KEY); -console.log('||||||||||||||||||||||||||||||||||||||'); -console.log('EXAMPLE_REMOTE_HOST1: ', process.env.EXAMPLE_REMOTE_HOST1); -console.log('EXAMPLE_REMOTE_USER1: ', process.env.EXAMPLE_REMOTE_USER1); -console.log('EXAMPLE_SSH_PRIVATE_KEY1: ', process.env.EXAMPLE_SSH_PRIVATE_KEY1); -console.log('||||||||||||||||||||||||||||||||||||||'); -console.log('REMOTE_USER: ', process.env.REMOTE_USER); -console.log('REMOTE_HOST: ', process.env.REMOTE_HOST); -console.log('SSH_PRIVATE_KEY: ', process.env.SSH_PRIVATE_KEY); -console.log('||||||||||||||||||||||||||||||||||||||');