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
This commit is contained in:
		
							parent
							
								
									a5d8edb941
								
							
						
					
					
						commit
						ec9347f8c6
					
				
							
								
								
									
										12
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								.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' | ||||
|   } | ||||
| }; | ||||
|  | ||||
							
								
								
									
										4
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @ -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 | ||||
|  | ||||
							
								
								
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @ -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 | ||||
|  | ||||
							
								
								
									
										11
									
								
								.github/workflows/e2e.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/e2e.yml
									
									
									
									
										vendored
									
									
								
							| @ -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 | ||||
|  | ||||
							
								
								
									
										8
									
								
								.github/workflows/manual-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/manual-release.yml
									
									
									
									
										vendored
									
									
								
							| @ -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 }} | ||||
|  | ||||
							
								
								
									
										29
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								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. | ||||
|  | ||||
							
								
								
									
										22
									
								
								action.yml
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								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: | ||||
|  | ||||
							
								
								
									
										2
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										65
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										65
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -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", | ||||
|  | ||||
| @ -30,7 +30,6 @@ | ||||
|   }, | ||||
|   "homepage": "https://github.com/easingthemes/ssh-deploy#readme", | ||||
|   "dependencies": { | ||||
|     "command-exists": "^1.2.9", | ||||
|     "rsyncwrapper": "^3.0.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|  | ||||
| @ -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); | ||||
|   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('✅ [SSH] dir created.'); | ||||
|   } else { | ||||
|     console.log(`[SSH] ${dir} dir exist`); | ||||
|   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 | ||||
| }; | ||||
|  | ||||
							
								
								
									
										108
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								src/index.js
									
									
									
									
									
								
							| @ -1,79 +1,47 @@ | ||||
| #!/usr/bin/env node
 | ||||
| const nodeRsync = require('rsyncwrapper'); | ||||
| 
 | ||||
| const { validateRsync, validateInputs } = require('./rsyncCli'); | ||||
| const { addSshKey } = require('./sshKey'); | ||||
| const { sshDeploy } = require('./rsyncCli'); | ||||
| const { remoteCmdBefore, remoteCmdAfter } = require('./remoteCmd'); | ||||
| const { addSshKey, getPrivateKeyPath, updateKnownHosts } = require('./sshKey'); | ||||
| const { validateRequiredInputs } = require('./helpers'); | ||||
| const inputs = require('./inputs'); | ||||
| 
 | ||||
| const run = async () => { | ||||
|   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); | ||||
|     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 | ||||
|   }); | ||||
|     } catch (err) { | ||||
|       console.error('⚠️ [Rsync] command error: ', err.message, err.stack); | ||||
|       process.abort(); | ||||
|   // Check script after
 | ||||
|   if (scriptAfter) { | ||||
|     await remoteCmdAfter(scriptAfter, privateKeyPath); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
|   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 }); | ||||
| run() | ||||
|   .then((data = '') => { | ||||
|     console.log('✅ [DONE]', data); | ||||
|   }) | ||||
|   .catch((error) => { | ||||
|     console.error('❌ [ERROR]', error.message); | ||||
|     process.exit(1); | ||||
|   }); | ||||
|   }; | ||||
| 
 | ||||
|   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
 | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| run(); | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										40
									
								
								src/remoteCmd.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/remoteCmd.js
									
									
									
									
									
										Normal file
									
								
							| @ -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') | ||||
| }; | ||||
							
								
								
									
										102
									
								
								src/rsyncCli.js
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								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(); | ||||
|   } | ||||
| 
 | ||||
|   console.log('⚠️ [CLI] Rsync doesn\'t exists. Start installation with "apt-get" \n'); | ||||
| 
 | ||||
|   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(); | ||||
| 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 { | ||||
|       console.log("✅ [CLI] Rsync installed. \n", data, stderr); | ||||
|       callback(); | ||||
|         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] 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}`); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| 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 | ||||
| }; | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
| }; | ||||
|  | ||||
							
								
								
									
										13
									
								
								src/test.js
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/test.js
									
									
									
									
									
								
							| @ -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('||||||||||||||||||||||||||||||||||||||'); | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user