├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── .yarnrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── shipit-cli │ ├── .npmignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── shipit │ ├── package.json │ ├── src │ │ ├── Shipit.js │ │ ├── Shipit.test.js │ │ ├── cli.js │ │ └── index.js │ └── tests │ │ ├── integration.test.js │ │ └── sandbox │ │ └── shipitfile.babel.js ├── shipit-deploy │ ├── .npmignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── __mocks__ │ │ └── tmp-promise.js │ ├── docs │ │ └── Windows.md │ ├── package.json │ ├── src │ │ ├── extendShipit.js │ │ ├── index.js │ │ └── tasks │ │ │ ├── deploy │ │ │ ├── clean.js │ │ │ ├── clean.test.js │ │ │ ├── fetch.js │ │ │ ├── fetch.test.js │ │ │ ├── finish.js │ │ │ ├── finish.test.js │ │ │ ├── index.js │ │ │ ├── init.js │ │ │ ├── init.test.js │ │ │ ├── publish.js │ │ │ ├── publish.test.js │ │ │ ├── update.js │ │ │ └── update.test.js │ │ │ ├── pending │ │ │ ├── index.js │ │ │ ├── log.js │ │ │ └── log.test.js │ │ │ └── rollback │ │ │ ├── finish.js │ │ │ ├── finish.test.js │ │ │ ├── index.js │ │ │ ├── init.js │ │ │ └── init.test.js │ └── tests │ │ ├── integration.test.js │ │ ├── sandbox │ │ └── shipitfile.babel.js │ │ └── util.js └── ssh-pool │ ├── .npmignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── __mocks__ │ ├── child_process.js │ ├── tmp.js │ └── which.js │ ├── examples │ └── hostname.js │ ├── package.json │ ├── src │ ├── Connection.js │ ├── Connection.test.js │ ├── ConnectionPool.js │ ├── ConnectionPool.test.js │ ├── commands │ │ ├── cd.js │ │ ├── cd.test.js │ │ ├── mkdir.js │ │ ├── mkdir.test.js │ │ ├── raw.js │ │ ├── raw.test.js │ │ ├── rm.js │ │ ├── rm.test.js │ │ ├── rsync.js │ │ ├── rsync.test.js │ │ ├── scp.js │ │ ├── scp.test.js │ │ ├── ssh.js │ │ ├── ssh.test.js │ │ ├── tar.js │ │ ├── tar.test.js │ │ ├── util.js │ │ └── util.test.js │ ├── index.js │ ├── remote.js │ ├── remote.test.js │ ├── util.js │ └── util.test.js │ └── tests │ ├── __fixtures__ │ └── test.txt │ └── integration.test.js ├── resources ├── shipit-logo-dark.png ├── shipit-logo-light.png └── shipit-logo.sketch ├── ssh ├── id_rsa └── id_rsa.pub └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | lib/ 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['airbnb-base', 'prettier'], 4 | parser: 'babel-eslint', 5 | parserOptions: { 6 | ecmaVersion: 8, 7 | sourceType: 'module', 8 | }, 9 | env: { 10 | jest: true, 11 | }, 12 | rules: { 13 | 'class-methods-use-this': 'off', 14 | 'import/prefer-default-export': 'off', 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: neoziro 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | packages/*/lib 3 | coverage/ 4 | .eslintcache 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | CHANGELOG.md 4 | package.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 8 5 | - 10 6 | 7 | before_install: 8 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.28.4 9 | - export PATH="$HOME/.yarn/bin:$PATH" 10 | 11 | script: 12 | - chmod 700 ssh && chmod 644 ssh/id_rsa.pub && chmod 600 ssh/id_rsa 13 | - yarn ci 14 | 15 | notifications: 16 | email: false 17 | 18 | cache: 19 | yarn: true 20 | directories: 21 | - '.eslintcache' 22 | - 'node_modules' 23 | 24 | addons: 25 | ssh_known_hosts: test.shipitjs.com 26 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | workspaces-experimental true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [5.3.0](https://github.com/shipitjs/shipit/compare/v5.2.0...v5.3.0) (2020-03-18) 7 | 8 | 9 | ### Features 10 | 11 | * add support of `asUser` ([#260](https://github.com/shipitjs/shipit/issues/260)) ([4e79edb](https://github.com/shipitjs/shipit/commit/4e79edb)) 12 | 13 | 14 | 15 | 16 | 17 | # [5.2.0](https://github.com/shipitjs/shipit/compare/v5.1.0...v5.2.0) (2020-03-07) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **windows:** cd must run the specified drive letter ([#252](https://github.com/shipitjs/shipit/issues/252)) ([ab916a9](https://github.com/shipitjs/shipit/commit/ab916a9)) 23 | * fix remote command wont reject on error, when cwd option is used ([#265](https://github.com/shipitjs/shipit/issues/265)) ([986aec1](https://github.com/shipitjs/shipit/commit/986aec1)) 24 | 25 | 26 | ### Features 27 | 28 | * add a config validation function ([#258](https://github.com/shipitjs/shipit/issues/258)) ([d98ec8e](https://github.com/shipitjs/shipit/commit/d98ec8e)) 29 | 30 | 31 | 32 | 33 | 34 | # [5.1.0](https://github.com/shipitjs/shipit/compare/v5.0.0...v5.1.0) (2019-08-28) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * correct peerDependencies field for shipit-deploy package ([#243](https://github.com/shipitjs/shipit/issues/243)) ([3586c21](https://github.com/shipitjs/shipit/commit/3586c21)) 40 | 41 | 42 | ### Features 43 | 44 | * **shipit-deploy:** Added config so you can rsync including the folder ([#246](https://github.com/shipitjs/shipit/issues/246)) ([64481f8](https://github.com/shipitjs/shipit/commit/64481f8)) 45 | * **ssh-pool:** Added ssh config array to remote server ([#248](https://github.com/shipitjs/shipit/issues/248)) ([ba1d8c2](https://github.com/shipitjs/shipit/commit/ba1d8c2)) 46 | 47 | 48 | 49 | 50 | 51 | # [4.2.0](https://github.com/shipitjs/shipit/compare/v4.1.4...v4.2.0) (2019-03-01) 52 | 53 | 54 | ### Features 55 | 56 | * add "init:after_ssh_pool" event ([#230](https://github.com/shipitjs/shipit/issues/230)) ([e864338](https://github.com/shipitjs/shipit/commit/e864338)) 57 | 58 | 59 | 60 | 61 | 62 | ## [4.1.4](https://github.com/shipitjs/shipit/compare/v4.1.3...v4.1.4) (2019-02-19) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * **shipit-deploy:** skip fetching git in case when repositoryUrl was not provided (closes [#207](https://github.com/shipitjs/shipit/issues/207)) ([#226](https://github.com/shipitjs/shipit/issues/226)) ([4ae0f89](https://github.com/shipitjs/shipit/commit/4ae0f89)) 68 | 69 | 70 | 71 | 72 | 73 | ## [4.1.3](https://github.com/shipitjs/shipit/compare/v4.1.2...v4.1.3) (2018-11-11) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * fixes directory permissions ([#224](https://github.com/shipitjs/shipit/issues/224)) ([3277adf](https://github.com/shipitjs/shipit/commit/3277adf)), closes [#189](https://github.com/shipitjs/shipit/issues/189) 79 | 80 | 81 | 82 | 83 | 84 | ## [4.1.2](https://github.com/shipitjs/shipit/compare/v4.1.1...v4.1.2) (2018-11-04) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * **security:** use which instead of whereis ([#220](https://github.com/shipitjs/shipit/issues/220)) ([6f46cad](https://github.com/shipitjs/shipit/commit/6f46cad)) 90 | * **shipit-deploy:** only remove workspace if not shallow clone ([#200](https://github.com/shipitjs/shipit/issues/200)) ([6ba6f00](https://github.com/shipitjs/shipit/commit/6ba6f00)) 91 | * use correct deprecation warning ([#219](https://github.com/shipitjs/shipit/issues/219)) ([e0c0fa5](https://github.com/shipitjs/shipit/commit/e0c0fa5)) 92 | 93 | 94 | 95 | 96 | 97 | 98 | ## [4.1.1](https://github.com/shipitjs/shipit/compare/v4.1.0...v4.1.1) (2018-05-30) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * update shipit-deploy's peerDependency to v4.1.0 ([#192](https://github.com/shipitjs/shipit/issues/192)) ([6f7b407](https://github.com/shipitjs/shipit/commit/6f7b407)) 104 | 105 | 106 | 107 | 108 | 109 | # [4.1.0](https://github.com/shipitjs/shipit/compare/v4.0.2...v4.1.0) (2018-04-27) 110 | 111 | 112 | ### Features 113 | 114 | * **ssh-pool:** add SSH Verbosity Levels ([#191](https://github.com/shipitjs/shipit/issues/191)) ([327c63e](https://github.com/shipitjs/shipit/commit/327c63e)) 115 | 116 | 117 | 118 | 119 | 120 | ## [4.0.2](https://github.com/shipitjs/shipit/compare/v4.0.1...v4.0.2) (2018-03-25) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * be compatible with CommonJS ([abd2316](https://github.com/shipitjs/shipit/commit/abd2316)) 126 | * fix scpCopyFromRemote & scpCopyToRemote ([01bc213](https://github.com/shipitjs/shipit/commit/01bc213)), closes [#178](https://github.com/shipitjs/shipit/issues/178) 127 | 128 | 129 | 130 | 131 | 132 | ## [4.0.1](https://github.com/shipitjs/shipit/compare/v4.0.0...v4.0.1) (2018-03-18) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * **shipit-cli:** correctly publish binary ([6b60f20](https://github.com/shipitjs/shipit/commit/6b60f20)) 138 | 139 | 140 | 141 | 142 | 143 | 144 | # 4.0.0 (2018-03-17) 145 | 146 | ## global 147 | 148 | ### Chores 149 | 150 | * Move to a Lerna repository 151 | * Add Codecov 152 | * Move to Jest for testing 153 | * Rewrite project in ES2017 targeting Node.js v6+ 154 | 155 | ## shipit-cli 156 | 157 | ### Features 158 | 159 | * Improve Shipit cli utilities #75 160 | * Support ES6 modules in shipitfile.babel.js 161 | * Give access to raw config #93 162 | * Standardize errors #154 163 | 164 | ### Fixes 165 | 166 | * Fix usage of user directory #160 167 | * Fix SSH key config shipitjs/shipit-deploy#151 shipitjs/shipit-deploy#126 168 | 169 | ### Docs 170 | 171 | * Improve documentation #69 #148 #81 172 | 173 | ### Deprecations 174 | 175 | * Deprecate `remoteCopy` in favor of `copyToRemote` and `copyFromRemote` 176 | 177 | ### BREAKING CHANGES 178 | 179 | * Drop callbacks support and use native Promises 180 | 181 | ## ssh-pool 182 | 183 | ### Features 184 | 185 | * Introduce a "tty" option in "run" method #56 186 | * Support "cwd" in "run" command #9 187 | * Expose a "isRsyncSupported" method 188 | 189 | ### Fixes 190 | 191 | * Fix parallel issues using scp copy shipitjs/ssh-pool#22 192 | * Fix command escaping #91 #152 193 | 194 | ### Docs 195 | 196 | * Update readme with new documentation 197 | 198 | ### Deprecations 199 | 200 | * Deprecate automatic "sudo" removing when using "asUser" #56 #12 201 | * Deprecate "copy" method in favor of "copyToRemote", "copyFromRemote", "scpCopyToRemote" and "scpCopyFromRemote" 202 | * Deprecate using "deploy" as default user 203 | * Deprecate automatic "tty" when detecting "sudo" #56 204 | 205 | ### BREAKING CHANGES 206 | 207 | * Drop callbacks support and use native Promises 208 | * Standardise errors #154 209 | * Replace "cwd" behaviour in "run" command #9 210 | 211 | ## shipit-deploy 212 | 213 | ### Fixes 214 | 215 | * Use [ instead of [[ to improve compatiblity shipitjs/shipit-deploy#147 shipitjs/shipit-deploy#148 216 | * Use rmfr to improve compatibility shipitjs/shipit-deploy#135 shipitjs/shipit-deploy#155 217 | 218 | ### BREAKING CHANGES 219 | 220 | * Default shallowClone to `true` 221 | * Drop grunt-shipit support 222 | * Workspace is now a temp directory in shallow clone 223 | * An error is thrown if workspace is set to the current directory 224 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Greg Bergé and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Shipit 3 |

4 |

Universal automation and deployment tool ⛵️

5 | 6 | [![Build Status][build-badge]][build] 7 | [![version][version-badge]][package] 8 | [![MIT License][license-badge]][license] 9 | 10 | [![PRs Welcome][prs-badge]][prs] 11 | 12 | [![Watch on GitHub][github-watch-badge]][github-watch] 13 | [![Star on GitHub][github-star-badge]][github-star] 14 | [![Tweet][twitter-badge]][twitter] 15 | 16 | ## Install shipit command line tools and shipit-deploy in your project 17 | 18 | ``` 19 | npm install --save-dev shipit-cli 20 | npm install --save-dev shipit-deploy 21 | ``` 22 | 23 | Shipit is an automation engine and a deployment tool. 24 | 25 | Shipit provides a good alternative to Capistrano or other build tools. It is easy to deploy or to automate simple tasks on your remote servers. 26 | 27 | **Features:** 28 | 29 | - Write your task using JavaScript 30 | - Task flow based on [orchestrator](https://github.com/orchestrator/orchestrator) 31 | - Login and interactive SSH commands 32 | - Easily extendable 33 | 34 | ## Deploy using Shipit 35 | 36 | 1. Create a `shipitfile.js` at the root of your project 37 | 38 | ```js 39 | // shipitfile.js 40 | module.exports = shipit => { 41 | // Load shipit-deploy tasks 42 | require('shipit-deploy')(shipit) 43 | 44 | shipit.initConfig({ 45 | default: { 46 | deployTo: '/var/apps/super-project', 47 | repositoryUrl: 'https://github.com/user/super-project.git', 48 | }, 49 | staging: { 50 | servers: 'deploy@staging.super-project.com', 51 | }, 52 | }) 53 | } 54 | ``` 55 | 56 | 2. Run deploy command using [npx](https://www.npmjs.com/package/npx): `npx shipit staging deploy` 57 | 58 | 3. You can rollback using `npx shipit staging rollback` 59 | 60 | ## Recipes 61 | 62 | ### Copy config file 63 | 64 | Add a custom task in your `shipitfile.js` and run `copyToRemote`. 65 | 66 | ```js 67 | // shipitfile.js 68 | module.exports = shipit => { 69 | /* ... */ 70 | 71 | shipit.task('copyConfig', async () => { 72 | await shipit.copyToRemote( 73 | 'config.json', 74 | '/var/apps/super-project/config.json', 75 | ) 76 | }) 77 | } 78 | ``` 79 | 80 | ### Use events 81 | 82 | You can add custom event and listen to events. 83 | 84 | ```js 85 | shipit.task('build', function() { 86 | // ... 87 | shipit.emit('built') 88 | }) 89 | 90 | shipit.on('built', function() { 91 | shipit.start('start-server') 92 | }) 93 | ``` 94 | 95 | Shipit emits the `init` event once initialized, before any tasks are run. 96 | 97 | ### Use Babel in your `shipitfile.js` 98 | 99 | Instead of using a `shipitfile.js`, use `shipitfile.babel.js`: 100 | 101 | ```js 102 | // shipitfile.babel.js 103 | export default shipit => { 104 | shipit.initConfig({ 105 | /* ... */ 106 | }) 107 | } 108 | ``` 109 | 110 | ### Customizing environments 111 | 112 | You can overwrite all default variables defined as part of the `default` object: 113 | 114 | ```js 115 | module.exports = shipit => { 116 | shipit.initConfig({ 117 | default: { 118 | branch: 'dev', 119 | }, 120 | staging: { 121 | servers: 'staging.myproject.com', 122 | workspace: '/home/vagrant/website' 123 | }, 124 | production: { 125 | servers: [{ 126 | host: 'app1.myproject.com', 127 | user: 'john', 128 | }, { 129 | host: 'app2.myproject.com', 130 | user: 'rob', 131 | }], 132 | branch: 'production', 133 | workspace: '/var/www/website' 134 | } 135 | }); 136 | 137 | ... 138 | shipit.task('pwd', function () { 139 | return shipit.remote('pwd'); 140 | }); 141 | ... 142 | }; 143 | ``` 144 | 145 | ### Asynchronous config 146 | 147 | If you can't call `shipit.initConfig(...)` right away because 148 | you need to get data asynchronously to do so, you can return 149 | a promise from the module: 150 | 151 | ```js 152 | module.exports = async shipit => { 153 | const servers = await getServers() 154 | shipit.initConfig({ 155 | production: { 156 | servers: servers, 157 | // ... 158 | }, 159 | }) 160 | } 161 | ``` 162 | 163 | ## Usage 164 | 165 | ``` 166 | Usage: shipit 167 | 168 | Options: 169 | 170 | -V, --version output the version number 171 | --shipitfile Specify a custom shipitfile to use 172 | --require Script required before launching Shipit 173 | --tasks List available tasks 174 | --environments List available environments 175 | -h, --help output usage information 176 | ``` 177 | 178 | ### Global configuration 179 | 180 | #### ignores 181 | 182 | Type: `Array` 183 | 184 | List of files excluded in `copyFromRemote` or `copyToRemote` methods. 185 | 186 | #### key 187 | 188 | Type: `String` 189 | 190 | Path to SSH key. 191 | 192 | #### servers 193 | 194 | Type: `String` or `Array` 195 | 196 | The server can use the shorthand syntax or an object: 197 | 198 | - `user@host`: user and host 199 | - `user@host:4000`: user, host and port 200 | - `{ user, host, port, extraSshOptions }`: an object 201 | 202 | ### Shipit Deploy configuration 203 | 204 | #### asUser 205 | 206 | Type: `String` 207 | 208 | Allows you to ‘become’ another user, different from the user that logged into the machine (remote user). 209 | 210 | #### deleteOnRollback 211 | 212 | Type: `Boolean`, default to `false` 213 | 214 | Delete release when a rollback is done. 215 | 216 | #### deployTo 217 | 218 | Type: `String` 219 | 220 | Directory where the code will be deployed on remote servers. 221 | 222 | #### keepReleases 223 | 224 | Type: `Number` 225 | 226 | Number of releases kept on remote servers. 227 | 228 | #### repositoryUrl 229 | 230 | Type: `String` 231 | 232 | Repository URL to clone, must be defined using `https` or `git+ssh` format. 233 | 234 | #### shallowClone 235 | 236 | Type: `Boolean`, default `true` 237 | 238 | Clone only the last commit of the repository. 239 | 240 | #### workspace 241 | 242 | Type: `String` 243 | 244 | If `shallowClone` is set to `false`, this directory will be used to clone the repository before deploying it. 245 | 246 | #### verboseSSHLevel 247 | 248 | Type: `Number`, default `0` 249 | 250 | SSH verbosity level to use when connecting to remote servers. **0** (none), **1** (-v), **2** (-vv), **3** (-vvv). 251 | 252 | ### API 253 | 254 | #### shipit.task(name, [deps], fn) 255 | 256 | Create a new Shipit task. If a promise is returned task will wait for completion. 257 | 258 | ```js 259 | shipit.task('hello', async () => { 260 | await shipit.remote('echo "hello on remote"') 261 | await shipit.local('echo "hello from local"') 262 | }) 263 | ``` 264 | 265 | #### shipit.blTask(name, [deps], fn) 266 | 267 | Create a new Shipit task that will block other tasks during its execution. If a promise is returned other task will wait before start. 268 | 269 | ```js 270 | shipit.blTask('hello', async () => { 271 | await shipit.remote('echo "hello on remote"') 272 | await shipit.local('echo "hello from local"') 273 | }) 274 | ``` 275 | 276 | #### shipit.start(tasks) 277 | 278 | Run Shipit tasks. 279 | 280 | ```js 281 | shipit.start('task') 282 | shipit.start('task1', 'task2') 283 | shipit.start(['task1', 'task2']) 284 | ``` 285 | 286 | #### shipit.local(command, [options]) 287 | 288 | Run a command locally and streams the result. See [ssh-pool#exec](https://github.com/shipitjs/shipit/tree/master/packages/ssh-pool#exec). 289 | 290 | ```js 291 | shipit 292 | .local('ls -lah', { 293 | cwd: '/tmp/deploy/workspace', 294 | }) 295 | .then(({ stdout }) => console.log(stdout)) 296 | .catch(({ stderr }) => console.error(stderr)) 297 | ``` 298 | 299 | #### shipit.remote(command, [options]) 300 | 301 | Run a command remotely and streams the result. See [ssh-pool#connection.run](https://github.com/shipitjs/shipit/tree/master/packages/ssh-pool#connectionruncommand-options). 302 | 303 | ```js 304 | shipit 305 | .remote('ls -lah') 306 | .then(([server1Result, server2Result]) => { 307 | console.log(server1Result.stdout) 308 | console.log(server2Result.stdout) 309 | }) 310 | .catch(error => { 311 | console.error(error.stderr) 312 | }) 313 | ``` 314 | 315 | #### shipit.copyToRemote(src, dest, [options]) 316 | 317 | Make a remote copy from a local path to a remote path. See [ssh-pool#connection.copyToRemote](https://github.com/shipitjs/shipit/tree/master/packages/ssh-pool#connectioncopytoremotesrc-dest-options). 318 | 319 | ```js 320 | shipit.copyToRemote('/tmp/workspace', '/opt/web/myapp') 321 | ``` 322 | 323 | #### shipit.copyFromRemote(src, dest, [options]) 324 | 325 | Make a remote copy from a remote path to a local path. See [ssh-pool#connection.copyFromRemote](https://github.com/shipitjs/shipit/tree/master/packages/ssh-pool#connectioncopyfromremotesrc-dest-options). 326 | 327 | ```js 328 | shipit.copyFromRemote('/opt/web/myapp', '/tmp/workspace') 329 | ``` 330 | 331 | #### shipit.log(...args) 332 | 333 | Log using Shipit, same API as `console.log`. 334 | 335 | ```js 336 | shipit.log('hello %s', 'world') 337 | ``` 338 | 339 | ## Dependencies 340 | 341 | - [OpenSSH](http://www.openssh.com/) 5+ 342 | - [rsync](https://rsync.samba.org/) 3+ 343 | 344 | ## Known Plugins 345 | 346 | ### Official 347 | 348 | - [shipit-deploy](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy) 349 | 350 | ### Third Party 351 | 352 | - [shipit-shared](https://github.com/timkelty/shipit-shared) 353 | - [shipit-db](https://github.com/timkelty/shipit-db) 354 | - [shipit-assets](https://github.com/timkelty/shipit-assets) 355 | - [shipit-ssh](https://github.com/timkelty/shipit-ssh) 356 | - [shipit-utils](https://github.com/timkelty/shipit-utils) 357 | - [shipit-npm](https://github.com/callerc1/shipit-npm) 358 | - [shipit-aws](https://github.com/KrashStudio/shipit-aws) 359 | - [shipit-captain](https://github.com/timkelty/shipit-captain/) 360 | - [shipit-bower](https://github.com/willsteinmetz/shipit-bower) 361 | - [shipit-composer](https://github.com/jeremyzahner/shipit-composer) 362 | - [shipit-bastion](https://github.com/BrokerageEngine/shipit-bastion) 363 | - [shipit-yaml](https://github.com/davidbernal/shipit-yaml) 364 | - [shipit-conditional](https://github.com/BrokerageEngine/shipit-conditional) 365 | 366 | ## Who use Shipit? 367 | 368 | - [Le Monde](http://www.lemonde.fr) 369 | - [Ghost blogging platform](https://ghost.org/) 370 | - [Fusionary](http://fusionary.com) 371 | 372 | ## License 373 | 374 | MIT 375 | 376 | [build-badge]: https://img.shields.io/travis/shipitjs/shipit.svg?style=flat-square 377 | [build]: https://travis-ci.org/shipitjs/shipit 378 | [version-badge]: https://img.shields.io/npm/v/shipit-cli.svg?style=flat-square 379 | [package]: https://www.npmjs.com/package/shipit-cli 380 | [license-badge]: https://img.shields.io/npm/l/shipit-cli.svg?style=flat-square 381 | [license]: https://github.com/shipitjs/shipit/blob/master/LICENSE 382 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 383 | [prs]: http://makeapullrequest.com 384 | [github-watch-badge]: https://img.shields.io/github/watchers/shipitjs/shipit.svg?style=social 385 | [github-watch]: https://github.com/shipitjs/shipit/watchers 386 | [github-star-badge]: https://img.shields.io/github/stars/shipitjs/shipit.svg?style=social 387 | [github-star]: https://github.com/shipitjs/shipit/stargazers 388 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20ShipitJS!%20https://github.com/shipitjs/shipit%20%F0%9F%91%8D 389 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/shipitjs/shipit.svg?style=social 390 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: '6', 8 | }, 9 | loose: true, 10 | }, 11 | ], 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | process.env.FORCE_COLOR=0; 2 | 3 | module.exports = { 4 | testEnvironment: 'node', 5 | roots: ['packages'], 6 | coverageDirectory: './coverage/', 7 | watchPlugins: [ 8 | 'jest-watch-typeahead/filename', 9 | 'jest-watch-typeahead/testname', 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.4.3", 3 | "npmClient": "yarn", 4 | "version": "5.3.0", 5 | "useWorkspaces": true 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "private": true, 4 | "scripts": { 5 | "postinstall": "chmod 400 ./ssh/id_rsa ./ssh/id_rsa.pub", 6 | "build": "lerna run build", 7 | "ci": "yarn build && yarn lint && yarn test --ci --coverage && codecov", 8 | "dev": "lerna run build --parallel -- --watch", 9 | "format": "prettier --write \"**/*.{js,json,md}\"", 10 | "lint": "eslint .", 11 | "release": "lerna publish --conventional-commits && conventional-github-releaser --preset angular", 12 | "test": "jest --runInBand" 13 | }, 14 | "devDependencies": { 15 | "@babel/cli": "^7.5.5", 16 | "@babel/core": "^7.5.5", 17 | "@babel/node": "^7.5.5", 18 | "@babel/preset-env": "^7.5.5", 19 | "babel-core": "^7.0.0-0", 20 | "babel-eslint": "^10.0.3", 21 | "babel-jest": "^24.9.0", 22 | "babel-preset-env": "^1.7.0", 23 | "chalk": "^2.4.1", 24 | "codecov": "^3.1.0", 25 | "conventional-github-releaser": "^3.1.2", 26 | "eslint": "^6.2.2", 27 | "eslint-config-airbnb-base": "^14.0.0", 28 | "eslint-config-prettier": "^6.1.0", 29 | "eslint-plugin-import": "^2.18.2", 30 | "glob": "^7.1.3", 31 | "jest": "^24.9.0", 32 | "jest-watch-typeahead": "^0.4.0", 33 | "lerna": "^3.16.4", 34 | "mkdirp": "^0.5.1", 35 | "mock-utf8-stream": "^0.1.1", 36 | "prettier": "^1.14.3", 37 | "std-mocks": "^1.0.1" 38 | }, 39 | "workspaces": [ 40 | "packages/*" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /packages/shipit-cli/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/lib/**/*.js 3 | !/bin/* 4 | *.test.js 5 | -------------------------------------------------------------------------------- /packages/shipit-cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [5.3.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v5.2.0...v5.3.0) (2020-03-18) 7 | 8 | 9 | ### Features 10 | 11 | * add support of `asUser` ([#260](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/260)) ([4e79edb](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/4e79edb)) 12 | 13 | 14 | 15 | 16 | 17 | # [5.2.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v5.1.0...v5.2.0) (2020-03-07) 18 | 19 | **Note:** Version bump only for package shipit-cli 20 | 21 | 22 | 23 | 24 | 25 | # [5.1.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v5.0.0...v5.1.0) (2019-08-28) 26 | 27 | **Note:** Version bump only for package shipit-cli 28 | 29 | 30 | 31 | 32 | 33 | # [4.2.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v4.1.4...v4.2.0) (2019-03-01) 34 | 35 | 36 | ### Features 37 | 38 | * add "init:after_ssh_pool" event ([#230](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/230)) ([e864338](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/e864338)) 39 | 40 | 41 | 42 | 43 | 44 | ## [4.1.2](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v4.1.1...v4.1.2) (2018-11-04) 45 | 46 | **Note:** Version bump only for package shipit-cli 47 | 48 | 49 | 50 | 51 | 52 | 53 | ## [4.1.1](https://github.com/shipitjs/shipit/compare/v4.1.0...v4.1.1) (2018-05-30) 54 | 55 | 56 | 57 | 58 | **Note:** Version bump only for package shipit-cli 59 | 60 | 61 | # [4.1.0](https://github.com/shipitjs/shipit/compare/v4.0.2...v4.1.0) (2018-04-27) 62 | 63 | 64 | ### Features 65 | 66 | * **ssh-pool:** add SSH Verbosity Levels ([#191](https://github.com/shipitjs/shipit/issues/191)) ([327c63e](https://github.com/shipitjs/shipit/commit/327c63e)) 67 | 68 | 69 | 70 | 71 | 72 | ## [4.0.2](https://github.com/shipitjs/shipit/compare/v4.0.1...v4.0.2) (2018-03-25) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * be compatible with CommonJS ([abd2316](https://github.com/shipitjs/shipit/commit/abd2316)) 78 | 79 | 80 | 81 | 82 | 83 | ## [4.0.1](https://github.com/shipitjs/shipit/compare/v4.0.0...v4.0.1) (2018-03-18) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * **shipit-cli:** correctly publish binary ([6b60f20](https://github.com/shipitjs/shipit/commit/6b60f20)) 89 | 90 | 91 | 92 | 93 | 94 | 95 | # 4.0.0 (2018-03-17) 96 | 97 | ### Features 98 | 99 | * Improve Shipit cli utilities #75 100 | * Support ES6 modules in shipitfile.babel.js 101 | * Give access to raw config #93 102 | * Standardize errors #154 103 | 104 | ### Fixes 105 | 106 | * Fix usage of user directory #160 107 | * Fix SSH key config shipitjs/shipit-deploy#151 shipitjs/shipit-deploy#126 108 | 109 | ### Chores 110 | 111 | * Move to a Lerna repository 112 | * Add Codecov 113 | * Move to Jest for testing 114 | * Rewrite project in ES2017 targeting Node.js v6+ 115 | 116 | ### Docs 117 | 118 | * Improve documentation #69 #148 #81 119 | 120 | ### Deprecations 121 | 122 | * Deprecate `remoteCopy` in favor of `copyToRemote` and `copyFromRemote` 123 | 124 | ### BREAKING CHANGES 125 | 126 | * Drop callbacks support and use native Promises 127 | -------------------------------------------------------------------------------- /packages/shipit-cli/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Greg Bergé and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/shipit-cli/README.md: -------------------------------------------------------------------------------- 1 | # shipit-cli 2 | 3 | [![Build Status][build-badge]][build] 4 | [![version][version-badge]][package] 5 | [![MIT License][license-badge]][license] 6 | 7 | Shipit command line interface. 8 | 9 | ``` 10 | npm install --save-dev shipit-cli 11 | ``` 12 | 13 | ## Usage 14 | 15 | ``` 16 | Usage: shipit 17 | 18 | Options: 19 | 20 | -V, --version output the version number 21 | --shipitfile Specify a custom shipitfile to use 22 | --require Script required before launching Shipit 23 | --tasks List available tasks 24 | --environments List available environments 25 | -h, --help output usage information 26 | ``` 27 | 28 | ## `shipitfile.js` 29 | 30 | ```js 31 | module.exports = shipit => { 32 | shipit.initConfig({ 33 | staging: { 34 | servers: 'myproject.com', 35 | }, 36 | }) 37 | 38 | shipit.task('pwd', async () => { 39 | await shipit.remote('pwd') 40 | }) 41 | } 42 | ``` 43 | 44 | ## API 45 | 46 | #### shipit.task(name, [deps], fn) 47 | 48 | Create a new Shipit task. If a promise is returned task will wait for completion. 49 | 50 | ```js 51 | shipit.task('hello', async () => { 52 | await shipit.remote('echo "hello on remote"') 53 | await shipit.local('echo "hello from local"') 54 | }) 55 | ``` 56 | 57 | #### shipit.blTask(name, [deps], fn) 58 | 59 | Create a new Shipit task that will block other tasks during its execution. If a promise is returned other task will wait before start. 60 | 61 | ```js 62 | shipit.blTask('hello', async () => { 63 | await shipit.remote('echo "hello on remote"') 64 | await shipit.local('echo "hello from local"') 65 | }) 66 | ``` 67 | 68 | #### shipit.start(tasks) 69 | 70 | Run Shipit tasks. 71 | 72 | ```js 73 | shipit.start('task') 74 | shipit.start('task1', 'task2') 75 | shipit.start(['task1', 'task2']) 76 | ``` 77 | 78 | #### shipit.local(command, [options]) 79 | 80 | Run a command locally and streams the result. See [ssh-pool#exec](https://github.com/shipitjs/shipit/tree/master/packages/ssh-pool#exec). 81 | 82 | ```js 83 | shipit 84 | .local('ls -lah', { 85 | cwd: '/tmp/deploy/workspace', 86 | }) 87 | .then(({ stdout }) => console.log(stdout)) 88 | .catch(({ stderr }) => console.error(stderr)) 89 | ``` 90 | 91 | #### shipit.remote(command, [options]) 92 | 93 | Run a command remotely and streams the result. Run a command locally and streams the result. See [ssh-pool#connection.run](https://github.com/shipitjs/shipit/tree/master/packages/ssh-pool#connectionruncommand-options). 94 | 95 | ```js 96 | shipit 97 | .remote('ls -lah') 98 | .then(([server1Result, server2Result]) => { 99 | console.log(server1Result.stdout) 100 | console.log(server2Result.stdout) 101 | }) 102 | .catch(error => { 103 | console.error(error.stderr) 104 | }) 105 | ``` 106 | 107 | #### shipit.copyToRemote(src, dest, [options]) 108 | 109 | Make a remote copy from a local path to a remote path. See [ssh-pool#connection.copyToRemote](https://github.com/shipitjs/shipit/tree/master/packages/ssh-pool#connectioncopytoremotesrc-dest-options). 110 | 111 | ```js 112 | shipit.copyToRemote('/tmp/workspace', '/opt/web/myapp') 113 | ``` 114 | 115 | #### shipit.copyFromRemote(src, dest, [options]) 116 | 117 | Make a remote copy from a remote path to a local path. See [ssh-pool#connection.copyFromRemote](https://github.com/shipitjs/shipit/tree/master/packages/ssh-pool#connectioncopyfromremotesrc-dest-options). 118 | 119 | ```js 120 | shipit.copyFromRemote('/opt/web/myapp', '/tmp/workspace') 121 | ``` 122 | 123 | #### shipit.log(...args) 124 | 125 | Log using Shipit, same API as `console.log`. 126 | 127 | ```js 128 | shipit.log('hello %s', 'world') 129 | ``` 130 | 131 | ## Workflow tasks 132 | 133 | When the system initializes it automatically emits events: 134 | 135 | - Emit event "init" 136 | - Emit event "init:after_ssh_pool" 137 | 138 | Each shipit task also generates events: 139 | 140 | - Emit event "task_start" 141 | - Emit event "task_stop" 142 | - Emit event "task_err" 143 | - Emit event "task_not_found" 144 | 145 | Inside the task events, you can test for the task name. 146 | 147 | ```js 148 | shipit.on('task_start', event => { 149 | if (event.task == 'first_task') { 150 | shipit.log("I'm the first task") 151 | } 152 | }) 153 | ``` 154 | 155 | ## License 156 | 157 | MIT 158 | 159 | [build-badge]: https://img.shields.io/travis/shipitjs/shipit.svg?style=flat-square 160 | [build]: https://travis-ci.org/shipitjs/shipit 161 | [version-badge]: https://img.shields.io/npm/v/shipit-cli.svg?style=flat-square 162 | [package]: https://www.npmjs.com/package/shipit-cli 163 | [license-badge]: https://img.shields.io/npm/l/shipit-cli.svg?style=flat-square 164 | [license]: https://github.com/shipitjs/shipit/blob/master/LICENSE 165 | -------------------------------------------------------------------------------- /packages/shipit-cli/bin/shipit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli') 4 | -------------------------------------------------------------------------------- /packages/shipit-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shipit-cli", 3 | "version": "5.3.0", 4 | "description": "Universal automation and deployment tool written in JavaScript.", 5 | "engines": { 6 | "node": ">=6" 7 | }, 8 | "author": "Greg Bergé ", 9 | "license": "MIT", 10 | "repository": "https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy", 11 | "main": "lib/index.js", 12 | "keywords": [ 13 | "shipit", 14 | "automation", 15 | "deployment", 16 | "deploy", 17 | "ssh" 18 | ], 19 | "scripts": { 20 | "prebuild": "rm -rf lib/", 21 | "build": "babel --config-file ../../babel.config.js -d lib --ignore \"**/*.test.js\" src", 22 | "prepublishOnly": "yarn run build" 23 | }, 24 | "bin": { 25 | "shipit": "./bin/shipit" 26 | }, 27 | "dependencies": { 28 | "chalk": "^2.4.1", 29 | "commander": "^3.0.0", 30 | "interpret": "^1.1.0", 31 | "liftoff": "^3.1.0", 32 | "orchestrator": "^0.3.7", 33 | "pretty-hrtime": "^1.0.0", 34 | "ssh-pool": "^5.3.0", 35 | "stream-line-wrapper": "^0.1.1", 36 | "v8flags": "^3.1.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/shipit-cli/src/Shipit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-underscore-dangle */ 2 | import { ConnectionPool, exec } from 'ssh-pool' 3 | import LineWrapper from 'stream-line-wrapper' 4 | import Orchestrator from 'orchestrator' 5 | import chalk from 'chalk' 6 | import prettyTime from 'pretty-hrtime' 7 | 8 | /** 9 | * An ExecResult returned when a command is executed with success. 10 | * @typedef {object} ExecResult 11 | * @property {Buffer} stdout 12 | * @property {Buffer} stderr 13 | * @property {ChildProcess} child 14 | */ 15 | 16 | /** 17 | * An ExecResult returned when a command is executed with success. 18 | * @typedef {object} MultipleExecResult 19 | * @property {Buffer} stdout 20 | * @property {Buffer} stderr 21 | * @property {ChildProcess[]} children 22 | */ 23 | 24 | /** 25 | * An ExecError returned when a command is executed with an error. 26 | * @typedef {Error} ExecError 27 | * @property {Buffer} stdout 28 | * @property {Buffer} stderr 29 | * @property {ChildProcess} child 30 | */ 31 | 32 | /** 33 | * Format orchestrator error. 34 | * 35 | * @param {Error} e 36 | * @returns {Error} 37 | */ 38 | function formatError(e) { 39 | if (!e.err) { 40 | return e.message 41 | } 42 | 43 | // PluginError 44 | if (typeof e.err.showStack === 'boolean') { 45 | return e.err.toString() 46 | } 47 | 48 | // normal error 49 | if (e.err.stack) { 50 | return e.err.stack 51 | } 52 | 53 | // unknown (string, number, etc.) 54 | return new Error(String(e.err)).stack 55 | } 56 | 57 | class Shipit extends Orchestrator { 58 | constructor(options) { 59 | super() 60 | 61 | const defaultOptions = { 62 | stdout: process.stdout, 63 | stderr: process.stderr, 64 | log: console.log.bind(console), 65 | } 66 | 67 | this.config = {} 68 | this.globalConfig = {} 69 | this.options = { ...defaultOptions, ...options } 70 | this.environment = options.environment 71 | 72 | this.initializeEvents() 73 | 74 | if (this.options.stdout === process.stdout) 75 | process.stdout.setMaxListeners(100) 76 | 77 | if (this.options.stderr === process.stderr) 78 | process.stderr.setMaxListeners(100) 79 | } 80 | 81 | /** 82 | * Initialize the `shipit`. 83 | * 84 | * @returns {Shipit} for chaining 85 | */ 86 | initialize() { 87 | if (!this.globalConfig[this.environment]) 88 | throw new Error(`Environment '${this.environment}' not found in config`) 89 | 90 | this.emit('init') 91 | return this.initSshPool() 92 | } 93 | 94 | /** 95 | * Initialize events. 96 | */ 97 | initializeEvents() { 98 | this.on('task_start', e => { 99 | // Specific log for noop functions. 100 | if (this.tasks[e.task].fn.toString() === 'function () {}') return 101 | 102 | this.log('\nRunning', `'${chalk.cyan(e.task)}' task...`) 103 | }) 104 | 105 | this.on('task_stop', e => { 106 | const task = this.tasks[e.task] 107 | // Specific log for noop functions. 108 | if (task.fn.toString() === 'function () {}') { 109 | this.log( 110 | 'Finished', 111 | `'${chalk.cyan(e.task)}'`, 112 | chalk.cyan(`[ ${task.dep.join(', ')} ]`), 113 | ) 114 | return 115 | } 116 | 117 | const time = prettyTime(e.hrDuration) 118 | this.log( 119 | 'Finished', 120 | `'${chalk.cyan(e.task)}'`, 121 | 'after', 122 | chalk.magenta(time), 123 | ) 124 | }) 125 | 126 | this.on('task_err', e => { 127 | const msg = formatError(e) 128 | const time = prettyTime(e.hrDuration) 129 | this.log( 130 | `'${chalk.cyan(e.task)}'`, 131 | chalk.red('errored after'), 132 | chalk.magenta(time), 133 | ) 134 | this.log(msg) 135 | }) 136 | 137 | this.on('task_not_found', err => { 138 | this.log(chalk.red(`Task '${err.task}' is not in your shipitfile`)) 139 | this.log( 140 | 'Please check the documentation for proper shipitfile formatting', 141 | ) 142 | }) 143 | } 144 | 145 | /** 146 | * Initialize SSH connections. 147 | * 148 | * @returns {Shipit} for chaining 149 | */ 150 | initSshPool() { 151 | if (!this.config.servers) throw new Error('Servers not filled') 152 | 153 | const servers = Array.isArray(this.config.servers) 154 | ? this.config.servers 155 | : [this.config.servers] 156 | 157 | const options = { 158 | ...this.options, 159 | key: this.config.key, 160 | asUser: this.config.asUser, 161 | strict: this.config.strict, 162 | verbosityLevel: 163 | this.config.verboseSSHLevel === undefined 164 | ? 0 165 | : this.config.verboseSSHLevel, 166 | } 167 | 168 | this.pool = new ConnectionPool(servers, options) 169 | 170 | this.emit('init:after_ssh_pool') 171 | return this 172 | } 173 | 174 | /** 175 | * Initialize shipit configuration. 176 | * 177 | * @param {object} config 178 | * @returns {Shipit} for chaining 179 | */ 180 | initConfig(config = {}) { 181 | this.globalConfig = config 182 | this.config = { 183 | ...config.default, 184 | ...config[this.environment], 185 | } 186 | return this 187 | } 188 | 189 | /** 190 | * Run a command locally. 191 | * 192 | * @param {string} command 193 | * @param {object} options 194 | * @returns {ChildObject} 195 | */ 196 | local(command, { stdout, stderr, ...cmdOptions } = {}) { 197 | this.log('Running "%s" on local.', command) 198 | const prefix = '@ ' 199 | return exec(command, cmdOptions, child => { 200 | if (this.options.stdout) 201 | child.stdout.pipe(new LineWrapper({ prefix })).pipe(this.options.stdout) 202 | 203 | if (this.options.stderr) 204 | child.stderr.pipe(new LineWrapper({ prefix })).pipe(this.options.stderr) 205 | }) 206 | } 207 | 208 | /** 209 | * Run a command remotely. 210 | * 211 | * @param {string} command 212 | * @returns {ExecResult} 213 | * @throws {ExecError} 214 | */ 215 | async remote(command, options) { 216 | return this.pool.run(command, options) 217 | } 218 | 219 | /** 220 | * Copy from local to remote or vice versa. 221 | * 222 | * @param {string} src 223 | * @param {string} dest 224 | * @returns {ExecResult|MultipleExecResult} 225 | * @throws {ExecError} 226 | */ 227 | async remoteCopy(src, dest, options) { 228 | const defaultOptions = { 229 | ignores: this.config && this.config.ignores ? this.config.ignores : [], 230 | rsync: this.config && this.config.rsync ? this.config.rsync : [], 231 | } 232 | const copyOptions = { ...defaultOptions, ...options } 233 | 234 | return this.pool.copy(src, dest, copyOptions) 235 | } 236 | 237 | /** 238 | * Run a copy from the local to the remote using rsync. 239 | * All exec options are also available. 240 | * 241 | * @see https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback 242 | * 243 | * @param {string} src 244 | * @param {string} dest 245 | * @param {object} [options] Options 246 | * @param {string[]} [options.ignores] Specify a list of files to ignore. 247 | * @param {string[]|string} [options.rsync] Specify a set of rsync arguments. 248 | * @returns {MultipleExecResult} 249 | * @throws {ExecError} 250 | */ 251 | async copyToRemote(src, dest, options) { 252 | const defaultOptions = { 253 | ignores: this.config && this.config.ignores ? this.config.ignores : [], 254 | rsync: this.config && this.config.rsync ? this.config.rsync : [], 255 | } 256 | const copyOptions = { ...defaultOptions, ...options } 257 | return this.pool.copyToRemote(src, dest, copyOptions) 258 | } 259 | 260 | /** 261 | * Run a copy from the remote to the local using rsync. 262 | * All exec options are also available. 263 | * 264 | * @see https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback 265 | * @param {string} src Source 266 | * @param {string} dest Destination 267 | * @param {object} [options] Options 268 | * @param {string[]} [options.ignores] Specify a list of files to ignore. 269 | * @param {string[]|string} [options.rsync] Specify a set of rsync arguments. 270 | * @returns {MultipleExecResult} 271 | * @throws {ExecError} 272 | */ 273 | async copyFromRemote(src, dest, options) { 274 | const defaultOptions = { 275 | ignores: this.config && this.config.ignores ? this.config.ignores : [], 276 | rsync: this.config && this.config.rsync ? this.config.rsync : [], 277 | } 278 | const copyOptions = { ...defaultOptions, ...options } 279 | return this.pool.copyFromRemote(src, dest, copyOptions) 280 | } 281 | 282 | /** 283 | * Log. 284 | * 285 | * @see console.log 286 | */ 287 | log(...args) { 288 | this.options.log(...args) 289 | } 290 | 291 | /** 292 | * Create a new blocking task. 293 | * 294 | * @see shipit.task 295 | */ 296 | blTask(name, ...rest) { 297 | this.task(name, ...rest) 298 | const task = this.tasks[name] 299 | task.blocking = true 300 | return task 301 | } 302 | 303 | /** 304 | * Test if we are ready to run a task. 305 | * Implement blocking task. 306 | */ 307 | _readyToRunTask(...args) { 308 | if ( 309 | Object.keys(this.tasks).some(key => { 310 | const task = this.tasks[key] 311 | return task.running === true && task.blocking === true 312 | }) 313 | ) 314 | return false 315 | 316 | return super._readyToRunTask(...args) // eslint-disable-line no-underscore-dangle 317 | } 318 | } 319 | 320 | export default Shipit 321 | -------------------------------------------------------------------------------- /packages/shipit-cli/src/Shipit.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import Stream from 'mock-utf8-stream' 3 | import { ConnectionPool } from 'ssh-pool' 4 | import Shipit from './Shipit' 5 | 6 | describe('Shipit', () => { 7 | let shipit 8 | let stdout 9 | let stderr 10 | 11 | beforeEach(() => { 12 | stdout = new Stream.MockWritableStream() 13 | stderr = new Stream.MockWritableStream() 14 | shipit = new Shipit({ 15 | stdout, 16 | stderr, 17 | environment: 'stage', 18 | log: jest.fn(), 19 | }) 20 | shipit.stage = 'stage' 21 | }) 22 | 23 | describe('#initConfig', () => { 24 | it('should set config and globalConfig', () => { 25 | const config = { 26 | default: { foo: 'bar', servers: ['1', '2'] }, 27 | stage: { kung: 'foo', servers: ['3'] }, 28 | } 29 | 30 | shipit.initConfig(config) 31 | 32 | expect(shipit.config).toEqual({ foo: 'bar', kung: 'foo', servers: ['3'] }) 33 | 34 | expect(shipit.globalConfig).toBe(config) 35 | }) 36 | }) 37 | 38 | describe('#initialize', () => { 39 | beforeEach(() => { 40 | shipit.initConfig({ stage: {} }) 41 | shipit.initSshPool = jest.fn(() => shipit) 42 | }) 43 | 44 | it('should return an error if environment is not found', () => { 45 | shipit.initConfig({}) 46 | expect(() => shipit.initialize()).toThrow( 47 | "Environment 'stage' not found in config", 48 | ) 49 | }) 50 | 51 | it('should add stage and initialize shipit', () => { 52 | shipit.initialize() 53 | expect(shipit.initSshPool).toBeCalled() 54 | }) 55 | it('should emit a "init" event', async () => { 56 | const spy = jest.fn() 57 | shipit.on('init', spy) 58 | expect(spy).toHaveBeenCalledTimes(0) 59 | shipit.initialize() 60 | expect(spy).toHaveBeenCalledTimes(1) 61 | }) 62 | }) 63 | 64 | describe('#initSshPool', () => { 65 | it('should initialize an ssh pool', () => { 66 | shipit.config = { servers: ['deploy@my-server'] } 67 | shipit.initSshPool() 68 | 69 | expect(shipit.pool).toEqual(expect.any(ConnectionPool)) 70 | expect(shipit.pool.connections[0].remote.user).toBe('deploy') 71 | expect(shipit.pool.connections[0].remote.host).toBe('my-server') 72 | }) 73 | it('should emit a "init:after_ssh_pool" event', async () => { 74 | shipit.config = { servers: ['deploy@my-server'] } 75 | const spy = jest.fn() 76 | shipit.on('init:after_ssh_pool', spy) 77 | expect(spy).toHaveBeenCalledTimes(0) 78 | shipit.initSshPool() 79 | expect(spy).toHaveBeenCalledTimes(1) 80 | }) 81 | }) 82 | 83 | describe('#local', () => { 84 | it('should wrap and log to stdout', async () => { 85 | stdout.startCapture() 86 | const res = await shipit.local('echo "hello"') 87 | expect(stdout.capturedData).toBe('@ hello\n') 88 | expect(res.stdout).toBeDefined() 89 | expect(res.stderr).toBeDefined() 90 | expect(res.child).toBeDefined() 91 | }) 92 | }) 93 | 94 | describe('#remote', () => { 95 | beforeEach(() => { 96 | shipit.pool = { run: jest.fn() } 97 | }) 98 | 99 | it('should run command on pool', () => { 100 | shipit.remote('my-command') 101 | 102 | expect(shipit.pool.run).toBeCalledWith('my-command', undefined) 103 | }) 104 | 105 | it('should support options', () => { 106 | shipit.remote('my-command', { cwd: '/my-directory' }) 107 | 108 | expect(shipit.pool.run).toBeCalledWith('my-command', { 109 | cwd: '/my-directory', 110 | }) 111 | }) 112 | }) 113 | 114 | describe('#remoteCopy', () => { 115 | beforeEach(() => { 116 | shipit.pool = { copy: jest.fn() } 117 | }) 118 | 119 | it('should run command on pool', () => { 120 | shipit.remoteCopy('src', 'dest') 121 | 122 | expect(shipit.pool.copy).toBeCalledWith('src', 'dest', { 123 | ignores: [], 124 | rsync: [], 125 | }) 126 | }) 127 | 128 | it('should accept options for shipit.pool.copy', () => { 129 | shipit.remoteCopy('src', 'dest', { 130 | direction: 'remoteToLocal', 131 | }) 132 | 133 | expect(shipit.pool.copy).toBeCalledWith('src', 'dest', { 134 | direction: 'remoteToLocal', 135 | ignores: [], 136 | rsync: [], 137 | }) 138 | }) 139 | 140 | it('should support options specified in config', () => { 141 | shipit.config = { 142 | ignores: ['foo'], 143 | rsync: ['--bar'], 144 | } 145 | 146 | shipit.remoteCopy('src', 'dest', { 147 | direction: 'remoteToLocal', 148 | }) 149 | 150 | expect(shipit.pool.copy).toBeCalledWith('src', 'dest', { 151 | direction: 'remoteToLocal', 152 | ignores: ['foo'], 153 | rsync: ['--bar'], 154 | }) 155 | }) 156 | }) 157 | 158 | describe('#copyFromRemote', () => { 159 | beforeEach(() => { 160 | shipit.pool = { copyFromRemote: jest.fn() } 161 | }) 162 | 163 | it('should run command on pool', () => { 164 | shipit.copyFromRemote('src', 'dest') 165 | 166 | expect(shipit.pool.copyFromRemote).toBeCalledWith('src', 'dest', { 167 | ignores: [], 168 | rsync: [], 169 | }) 170 | }) 171 | 172 | it('should accept options for shipit.pool.copyFromRemote', () => { 173 | shipit.copyFromRemote('src', 'dest', { 174 | ignores: ['foo'], 175 | }) 176 | 177 | expect(shipit.pool.copyFromRemote).toBeCalledWith('src', 'dest', { 178 | ignores: ['foo'], 179 | rsync: [], 180 | }) 181 | }) 182 | 183 | it('should support options specified in config', () => { 184 | shipit.config = { 185 | ignores: ['foo'], 186 | rsync: ['--bar'], 187 | } 188 | 189 | shipit.copyFromRemote('src', 'dest') 190 | 191 | expect(shipit.pool.copyFromRemote).toBeCalledWith('src', 'dest', { 192 | ignores: ['foo'], 193 | rsync: ['--bar'], 194 | }) 195 | }) 196 | }) 197 | 198 | describe('#copyToRemote', () => { 199 | beforeEach(() => { 200 | shipit.pool = { copyToRemote: jest.fn() } 201 | }) 202 | 203 | it('should run command on pool', () => { 204 | shipit.copyToRemote('src', 'dest') 205 | 206 | expect(shipit.pool.copyToRemote).toBeCalledWith('src', 'dest', { 207 | ignores: [], 208 | rsync: [], 209 | }) 210 | }) 211 | 212 | it('should accept options for shipit.pool.copyToRemote', () => { 213 | shipit.copyToRemote('src', 'dest', { 214 | ignores: ['foo'], 215 | }) 216 | 217 | expect(shipit.pool.copyToRemote).toBeCalledWith('src', 'dest', { 218 | ignores: ['foo'], 219 | rsync: [], 220 | }) 221 | }) 222 | 223 | it('should support options specified in config', () => { 224 | shipit.config = { 225 | ignores: ['foo'], 226 | rsync: ['--bar'], 227 | } 228 | 229 | shipit.copyToRemote('src', 'dest') 230 | 231 | expect(shipit.pool.copyToRemote).toBeCalledWith('src', 'dest', { 232 | ignores: ['foo'], 233 | rsync: ['--bar'], 234 | }) 235 | }) 236 | }) 237 | }) 238 | -------------------------------------------------------------------------------- /packages/shipit-cli/src/cli.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import chalk from 'chalk' 3 | import interpret from 'interpret' 4 | import v8flags from 'v8flags' 5 | import Liftoff from 'liftoff' 6 | import program from 'commander' 7 | import Shipit from './Shipit' 8 | import pkg from '../package.json' 9 | 10 | function exit(code) { 11 | if (process.platform === 'win32' && process.stdout.bufferSize) { 12 | process.stdout.once('drain', () => { 13 | process.exit(code) 14 | }) 15 | return 16 | } 17 | 18 | process.exit(code) 19 | } 20 | 21 | program 22 | .version(pkg.version) 23 | .allowUnknownOption() 24 | .usage(' ') 25 | .option('--shipitfile ', 'Specify a custom shipitfile to use') 26 | .option('--require ', 'Script required before launching Shipit') 27 | .option('--tasks', 'List available tasks') 28 | .option('--environments', 'List available environments') 29 | 30 | program.parse(process.argv) 31 | 32 | if (!process.argv.slice(2).length) { 33 | program.help() 34 | } 35 | 36 | function logTasks(shipit) { 37 | console.log( 38 | Object.keys(shipit.tasks) 39 | .join('\n') 40 | .trim(), 41 | ) 42 | } 43 | 44 | function logEnvironments(shipit) { 45 | console.log( 46 | Object.keys(shipit.globalConfig) 47 | .join('\n') 48 | .trim(), 49 | ) 50 | } 51 | 52 | async function asyncInvoke(env) { 53 | if (!env.configPath) { 54 | console.error(chalk.red('shipitfile not found')) 55 | exit(1) 56 | } 57 | 58 | const [environment, ...tasks] = program.args 59 | 60 | const shipit = new Shipit({ environment }) 61 | 62 | try { 63 | /* eslint-disable global-require, import/no-dynamic-require */ 64 | const module = require(env.configPath) 65 | /* eslint-enable global-require, import/no-dynamic-require */ 66 | const initialize = 67 | typeof module.default === 'function' ? module.default : module 68 | await initialize(shipit) 69 | } catch (error) { 70 | console.error(chalk.red('Could not load async config')) 71 | throw error 72 | } 73 | 74 | if (program.tasks === true) { 75 | logTasks(shipit) 76 | } else if (program.environments === true) { 77 | logEnvironments(shipit) 78 | } else { 79 | // Run the 'default' task if no task is specified 80 | const runTasks = tasks.length === 0 ? ['default'] : tasks 81 | 82 | shipit.initialize() 83 | 84 | shipit.on('task_err', () => exit(1)) 85 | shipit.on('task_not_found', () => exit(1)) 86 | 87 | shipit.start(runTasks) 88 | } 89 | } 90 | 91 | function invoke(env) { 92 | asyncInvoke(env).catch(error => { 93 | setTimeout(() => { 94 | throw error 95 | }) 96 | }) 97 | } 98 | 99 | const cli = new Liftoff({ 100 | name: 'shipit', 101 | extensions: interpret.jsVariants, 102 | v8flags, 103 | }) 104 | cli.launch( 105 | { 106 | configPath: program.shipitfile, 107 | require: program.require, 108 | }, 109 | invoke, 110 | ) 111 | -------------------------------------------------------------------------------- /packages/shipit-cli/src/index.js: -------------------------------------------------------------------------------- 1 | import Shipit from './Shipit' 2 | 3 | module.exports = Shipit 4 | -------------------------------------------------------------------------------- /packages/shipit-cli/tests/integration.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { exec } from 'ssh-pool' 3 | 4 | const shipitCli = path.resolve(__dirname, '../src/cli.js') 5 | const shipitFile = path.resolve(__dirname, './sandbox/shipitfile.babel.js') 6 | const babelNode = require.resolve('@babel/node/bin/babel-node'); 7 | 8 | describe('shipit-cli', () => { 9 | it('should run a local task', async () => { 10 | let { stdout } = await exec(`FORCE_COLOR=0 ${babelNode} ${shipitCli} --shipitfile ${shipitFile} test localHello`) 11 | stdout = stdout.trim(); 12 | 13 | expect(stdout).toMatch(/Running 'localHello' task.../) 14 | expect(stdout).toMatch(/Running "echo "hello"" on local./) 15 | expect(stdout).toMatch(/@ hello/) 16 | expect(stdout).toMatch(/Finished 'localHello' after/) 17 | }, 10000) 18 | 19 | it('should run a remote task', async () => { 20 | let { stdout } = await exec(`FORCE_COLOR=0 ${babelNode} ${shipitCli} --shipitfile ${shipitFile} test remoteUser`) 21 | stdout = stdout.trim(); 22 | 23 | expect(stdout).toMatch(/Running 'remoteUser' task.../) 24 | expect(stdout).toMatch(/Running "echo \$USER" on host "test.shipitjs.com"./) 25 | expect(stdout).toMatch(/@test.shipitjs.com deploy/) 26 | expect(stdout).toMatch(/Finished 'remoteUser' after/) 27 | }, 10000) 28 | 29 | it('should work with "~"', async () => { 30 | const { stdout } = await exec( 31 | `${babelNode} ${shipitCli} --shipitfile ${shipitFile} test cwdSsh`, 32 | ) 33 | expect(stdout).toMatch(/@test.shipitjs.com \/home\/deploy\/\.ssh/) 34 | }, 10000) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/shipit-cli/tests/sandbox/shipitfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | export default shipit => { 4 | shipit.initConfig({ 5 | default: { 6 | key: './ssh/id_rsa', 7 | }, 8 | test: { 9 | servers: 'deploy@test.shipitjs.com', 10 | }, 11 | }) 12 | 13 | shipit.task('localHello', async () => { 14 | await shipit.local('echo "hello"') 15 | }) 16 | 17 | shipit.task('remoteUser', async () => { 18 | await shipit.remote('echo $USER') 19 | }) 20 | 21 | shipit.task('cwdSsh', async () => { 22 | await shipit.remote('pwd', { cwd: '~/.ssh' }) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /packages/shipit-deploy/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/lib/**/*.js 3 | *.test.js 4 | -------------------------------------------------------------------------------- /packages/shipit-deploy/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [5.3.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v5.2.0...v5.3.0) (2020-03-18) 7 | 8 | 9 | ### Features 10 | 11 | * add support of `asUser` ([#260](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/260)) ([4e79edb](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/4e79edb)) 12 | 13 | 14 | 15 | 16 | 17 | # [5.2.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v5.1.0...v5.2.0) (2020-03-07) 18 | 19 | 20 | ### Features 21 | 22 | * add a config validation function ([#258](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/258)) ([d98ec8e](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/d98ec8e)) 23 | 24 | 25 | 26 | 27 | 28 | # [5.1.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v5.0.0...v5.1.0) (2019-08-28) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * correct peerDependencies field for shipit-deploy package ([#243](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/243)) ([3586c21](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/3586c21)) 34 | 35 | 36 | ### Features 37 | 38 | * **shipit-deploy:** Added config so you can rsync including the folder ([#246](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/246)) ([64481f8](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/64481f8)) 39 | 40 | 41 | 42 | 43 | 44 | ## [4.1.4](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v4.1.3...v4.1.4) (2019-02-19) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * **shipit-deploy:** skip fetching git in case when repositoryUrl was not provided (closes [#207](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/207)) ([#226](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/226)) ([4ae0f89](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/4ae0f89)) 50 | 51 | 52 | 53 | 54 | 55 | ## [4.1.3](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v4.1.2...v4.1.3) (2018-11-11) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * fixes directory permissions ([#224](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/224)) ([3277adf](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/3277adf)), closes [#189](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/189) 61 | 62 | 63 | 64 | 65 | 66 | ## [4.1.2](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v4.1.1...v4.1.2) (2018-11-04) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * **shipit-deploy:** only remove workspace if not shallow clone ([#200](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/200)) ([6ba6f00](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/6ba6f00)) 72 | 73 | 74 | 75 | 76 | 77 | 78 | ## [4.1.1](https://github.com/shipitjs/shipit/compare/v4.1.0...v4.1.1) (2018-05-30) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * update shipit-deploy's peerDependency to v4.1.0 ([#192](https://github.com/shipitjs/shipit/issues/192)) ([6f7b407](https://github.com/shipitjs/shipit/commit/6f7b407)) 84 | 85 | 86 | 87 | 88 | 89 | # [4.1.0](https://github.com/shipitjs/shipit/compare/v4.0.2...v4.1.0) (2018-04-27) 90 | 91 | 92 | 93 | 94 | **Note:** Version bump only for package shipit-deploy 95 | 96 | 97 | ## [4.0.2](https://github.com/shipitjs/shipit/compare/v4.0.1...v4.0.2) (2018-03-25) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * be compatible with CommonJS ([abd2316](https://github.com/shipitjs/shipit/commit/abd2316)) 103 | 104 | 105 | 106 | 107 | 108 | 109 | # 4.0.0 (2018-03-17) 110 | 111 | ## shipit-deploy 112 | 113 | ### Fixes 114 | 115 | * Use [ instead of [[ to improve compatiblity shipitjs/shipit-deploy#147 shipitjs/shipit-deploy#148 116 | * Use rmfr to improve compatibility shipitjs/shipit-deploy#135 shipitjs/shipit-deploy#155 117 | 118 | ### Chores 119 | 120 | * Move to a Lerna repository 121 | * Add Codecov 122 | * Move to Jest for testing 123 | * Rewrite project in ES2017 targeting Node.js v6+ 124 | 125 | ### BREAKING CHANGES 126 | 127 | * Default shallowClone to `true` 128 | * Drop grunt-shipit support 129 | * Workspace is now a temp directory in shallow clone 130 | * An error is thrown if workspace is set to the current directory 131 | -------------------------------------------------------------------------------- /packages/shipit-deploy/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Greg Bergé and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/shipit-deploy/README.md: -------------------------------------------------------------------------------- 1 | # shipit-deploy 2 | 3 | [![Build Status][build-badge]][build] 4 | [![version][version-badge]][package] 5 | [![MIT License][license-badge]][license] 6 | 7 | Set of deployment tasks for [Shipit](https://github.com/shipitjs/shipit). 8 | 9 | **Features:** 10 | 11 | - Deploy tag, branch or commit 12 | - Add additional behaviour using hooks 13 | - Build your project locally or remotely 14 | - Easy rollback 15 | 16 | ## Install 17 | 18 | ``` 19 | npm install shipit-deploy 20 | ``` 21 | 22 | If you are deploying from Windows, you may want to have a look at the [wiki page about usage in Windows](https://github.com/shipitjs/shipit/blob/master/packages/shipit-deploy/docs/Windows.md). 23 | 24 | ## Usage 25 | 26 | ### Example `shipitfile.js` 27 | 28 | ```js 29 | module.exports = shipit => { 30 | require('shipit-deploy')(shipit) 31 | 32 | shipit.initConfig({ 33 | default: { 34 | workspace: '/tmp/myapp', 35 | deployTo: '/var/myapp', 36 | repositoryUrl: 'https://github.com/user/myapp.git', 37 | ignores: ['.git', 'node_modules'], 38 | keepReleases: 2, 39 | keepWorkspace: false, // should we remove workspace dir after deploy? 40 | deleteOnRollback: false, 41 | key: '/path/to/key', 42 | shallowClone: true, 43 | deploy: { 44 | remoteCopy: { 45 | copyAsDir: false, // Should we copy as the dir (true) or the content of the dir (false) 46 | }, 47 | }, 48 | }, 49 | staging: { 50 | servers: 'user@myserver.com', 51 | }, 52 | }) 53 | } 54 | ``` 55 | 56 | To deploy on staging, you must use the following command : 57 | 58 | ``` 59 | shipit staging deploy 60 | ``` 61 | 62 | You can rollback to the previous releases with the command : 63 | 64 | ``` 65 | shipit staging rollback 66 | ``` 67 | 68 | ## Options 69 | 70 | ### workspace 71 | 72 | Type: `String` 73 | 74 | Define a path to a directory where Shipit builds it's syncing source. 75 | 76 | > **Beware to not set this path to the root of your repository (unless you are set `keepWorkspace: true`) as shipit-deploy cleans the directory at the given path after successful deploy.** 77 | 78 | Here you have the following setup possibilities: 79 | 80 | - if you want to build and deploy from the directory with your repo: 81 | - set `keepWorkspace: true` so that your workspace dir won't be removed after deploy 82 | - optionally set `rsyncFrom` if you want to sync e.g. only `./build` dir 83 | - set `branch` so that we can get correct revision hash 84 | - if you want every time to fetch a fresh repo copy and dun reploy on it: 85 | - set `shallowClone: true` — this will speed up repo fetch speed and create a temporary workspace. **NOTE:** if you decide not to use `shallowClone`, you should set `workspace` path manually. If you set `shallowClone: true`, then the temporary workspace directory will be removed after deploy (unless you set `keepWorkspace: true`) 86 | - set `repositoryUrl` and optionally `branch` and `gitConfig` 87 | 88 | ### keepWorkspace 89 | 90 | Type: `Boolean` 91 | 92 | If `true` — we won't remove workspace dir after deploy. 93 | 94 | ### dirToCopy 95 | 96 | Type: `String` 97 | Default: same as workspace 98 | 99 | Define directory within the workspace which should be deployed. 100 | 101 | ### deployTo 102 | 103 | Type: `String` 104 | 105 | Define the remote path where the project will be deployed. A directory `releases` is automatically created. A symlink `current` is linked to the current release. 106 | 107 | ### repositoryUrl 108 | 109 | Type: `String` 110 | 111 | Git URL of the project repository. 112 | 113 | If empty Shipit will try to deploy without pulling the changes. 114 | 115 | In edge cases like quick PoC projects without a repository or a living on the edge production patch applying this can be helpful. 116 | 117 | ### branch 118 | 119 | Type: `String` 120 | 121 | Tag, branch or commit to deploy. 122 | 123 | ### ignores 124 | 125 | Type: `Array` 126 | 127 | An array of paths that match ignored files. These paths are used in the rsync command. 128 | 129 | ### deleteOnRollback 130 | 131 | Type: `Boolean` 132 | 133 | Whether or not to delete the old release when rolling back to a previous release. 134 | 135 | ### key 136 | 137 | Type: `String` 138 | 139 | Path to SSH key 140 | 141 | ### keepReleases 142 | 143 | Type: `Number` 144 | 145 | Number of releases to keep on the remote server. 146 | 147 | ### shallowClone 148 | 149 | Type: `Boolean` 150 | 151 | Perform a shallow clone. Default: `false`. 152 | 153 | ### updateSubmodules 154 | 155 | Type: Boolean 156 | 157 | Update submodules. Default: `false`. 158 | 159 | ### gitConfig 160 | 161 | type: `Object` 162 | 163 | Custom git configuration settings for the cloned repo. 164 | 165 | ### gitLogFormat 166 | 167 | Type: `String` 168 | 169 | Log format to pass to [`git log`](http://git-scm.com/docs/git-log#_pretty_formats). Used to display revision diffs in `pending` task. Default: `%h: %s - %an`. 170 | 171 | ### rsyncFrom 172 | 173 | Type: `String` _Optional_ 174 | 175 | When deploying from Windows, prepend the workspace path with the drive letter. For example `/d/tmp/workspace` if your workspace is located in `d:\tmp\workspace`. 176 | By default, it will run rsync from the workspace folder. 177 | 178 | ### copy 179 | 180 | Type: `String` 181 | 182 | Parameter to pass to `cp` to copy the previous release. Non NTFS filesystems support `-r`. Default: `-a` 183 | 184 | ### deploy.remoteCopy.copyAsDir 185 | 186 | Type: `Boolean` _Optional_ 187 | 188 | If `true` - We will copy the folder instead of the content of the folder. Default: `false`. 189 | 190 | ## Variables 191 | 192 | Several variables are attached during the deploy and the rollback process: 193 | 194 | ### shipit.config.\* 195 | 196 | All options described in the config sections are available in the `shipit.config` object. 197 | 198 | ### shipit.repository 199 | 200 | Attached during `deploy:fetch` task. 201 | 202 | You can manipulate the repository using git command, the API is describe in [gift](https://github.com/sentientwaffle/gift). 203 | 204 | ### shipit.releaseDirname 205 | 206 | Attached during `deploy:update` and `rollback:init` task. 207 | 208 | The current release dirname of the project, the format used is "YYYYMMDDHHmmss" (moment format). 209 | 210 | ### shipit.releasesPath 211 | 212 | Attached during `deploy:init`, `rollback:init`, and `pending:log` tasks. 213 | 214 | The remote releases path. 215 | 216 | ### shipit.releasePath 217 | 218 | Attached during `deploy:update` and `rollback:init` task. 219 | 220 | The complete release path : `path.join(shipit.releasesPath, shipit.releaseDirname)`. 221 | 222 | ### shipit.currentPath 223 | 224 | Attached during `deploy:init`, `rollback:init`, and `pending:log` tasks. 225 | 226 | The current symlink path : `path.join(shipit.config.deployTo, 'current')`. 227 | 228 | ## Workflow tasks 229 | 230 | - deploy 231 | - deploy:init 232 | - Emit event "deploy". 233 | - deploy:fetch 234 | - Create workspace. 235 | - Initialize repository. 236 | - Add remote. 237 | - Fetch repository. 238 | - Checkout commit-ish. 239 | - Merge remote branch in local branch. 240 | - Emit event "fetched". 241 | - deploy:update 242 | - Create and define release path. 243 | - Remote copy project. 244 | - Emit event "updated". 245 | - deploy:publish 246 | - Update symlink. 247 | - Emit event "published". 248 | - deploy:clean 249 | - Remove old releases. 250 | - Emit event "cleaned". 251 | - deploy:finish 252 | - Emit event "deployed". 253 | - rollback 254 | - rollback:init 255 | - Define release path. 256 | - Emit event "rollback". 257 | - deploy:publish 258 | - Update symlink. 259 | - Emit event "published". 260 | - deploy:clean 261 | - Remove old releases. 262 | - Emit event "cleaned". 263 | - rollback:finish 264 | - Emit event "rollbacked". 265 | - pending 266 | - pending:log 267 | - Log pending commits (diff between HEAD and currently deployed revision) to console. 268 | 269 | ## License 270 | 271 | MIT 272 | 273 | [build-badge]: https://img.shields.io/travis/shipitjs/shipit.svg?style=flat-square 274 | [build]: https://travis-ci.org/shipitjs/shipit 275 | [version-badge]: https://img.shields.io/npm/v/shipit-deploy.svg?style=flat-square 276 | [package]: https://www.npmjs.com/package/shipit-deploy 277 | [license-badge]: https://img.shields.io/npm/l/shipit-deploy.svg?style=flat-square 278 | [license]: https://github.com/shipitjs/shipit/blob/master/LICENSE 279 | -------------------------------------------------------------------------------- /packages/shipit-deploy/__mocks__/tmp-promise.js: -------------------------------------------------------------------------------- 1 | export default { 2 | async dir() { 3 | return { 4 | path: '/tmp/workspace-generated', 5 | cleanup: jest.fn(async () => {}), 6 | } 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /packages/shipit-deploy/docs/Windows.md: -------------------------------------------------------------------------------- 1 | # Deploy from Windows 2 | 3 | Requires Powershell 4 | 5 | ## Install [Scoop](https://github.com/lukesampson/scoop) 6 | 7 | ``` 8 | iex (new-object net.webclient).downloadstring('https://get.scoop.sh') 9 | set-executionpolicy unrestricted -s cu 10 | ``` 11 | 12 | ## Install programs 13 | 14 | OpenSSH, rsync and [pshazz](https://github.com/lukesampson/pshazz) 15 | 16 | ``` 17 | scoop install openssh 18 | scoop install rsync 19 | scoop install pshazz 20 | ``` 21 | 22 | ## Configure shipit-deploy 23 | 24 | Here are some advices _(replace everything between <<>> with your own values)_: 25 | 26 | ### key 27 | 28 | Path can be an absolute windows-style path, just replace `\` by `/` 29 | 30 | ``` 31 | "key" : "c:/users/<>/.ssh/id_rsa" 32 | ``` 33 | 34 | ### Workspace 35 | 36 | Here is where I had problems. On one side, shipit will use the current drive letter as the root for folders. On the other side, rsync expects paths with drive letters in them. Here is what I tried (assuming project is located on `D:` drive): 37 | 38 | #### `"workspace" : "/workspace/"` 39 | 40 | - Copies project to `d:\workspace` 41 | - rsync fails to find the folder 42 | 43 | #### `"workspace" : "d:/workspace/"` 44 | 45 | - Copies project to `d:\workspace` 46 | - rsync fails to find the folder 47 | 48 | #### `"workspace" : "/d/workspace/"` 49 | 50 | - rsync would work and properly sync from `D:\workspace` but... 51 | - ... the copy of the project goes Copies project to `d:\d\workspace` 52 | 53 | In the end, I had picked option 1. and I had to make a change in the source code to append the drive letter to rsync's source path. Everything is described in [that commit of my pull request](https://github.com/vpratfr/shipit-deploy/commit/4bbf262a7d7036a2b534ab7233d0152e0d09ba20). You can then simply add a configuration variable to "default": `"rsyncDrive": "/d"`. 54 | 55 | ## Troubleshooting 56 | 57 | ### Conflict between ssh executables 58 | 59 | Rsync prefers to use its own ssh.exe version. If gitbash is installed (or any other program offering its own ssh.exe), make sure that the ssh.exe used by default is the one from the rsync folder. 60 | 61 | Hint: this could be done by making sure the rsync folder comes first in the PATH 62 | -------------------------------------------------------------------------------- /packages/shipit-deploy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shipit-deploy", 3 | "version": "5.3.0", 4 | "description": "Official set of deploy tasks for Shipit.", 5 | "engines": { 6 | "node": ">=6" 7 | }, 8 | "author": "Greg Bergé ", 9 | "license": "MIT", 10 | "repository": "https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy", 11 | "main": "lib/index.js", 12 | "keywords": [ 13 | "shipit", 14 | "automation", 15 | "deployment", 16 | "deploy", 17 | "ssh" 18 | ], 19 | "scripts": { 20 | "prebuild": "rm -rf lib/", 21 | "build": "babel --config-file ../../babel.config.js -d lib --ignore \"**/*.test.js\" src", 22 | "prepublishOnly": "yarn run build" 23 | }, 24 | "peerDependencies": { 25 | "shipit-cli": "^5.0.0" 26 | }, 27 | "dependencies": { 28 | "chalk": "^2.4.1", 29 | "lodash": "^4.17.15", 30 | "moment": "^2.21.0", 31 | "path2": "^0.1.0", 32 | "rmfr": "^2.0.0", 33 | "shipit-utils": "^1.1.3", 34 | "tmp-promise": "^2.0.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/extendShipit.js: -------------------------------------------------------------------------------- 1 | import path from 'path2/posix' 2 | import _ from 'lodash' 3 | import util from 'util' 4 | 5 | /** 6 | * Compute the current release dir name. 7 | * 8 | * @param {object} result 9 | * @returns {string} 10 | */ 11 | function computeReleases(result) { 12 | if (!result.stdout) return null 13 | 14 | // Trim last breakline. 15 | const dirs = result.stdout.replace(/\n$/, '') 16 | 17 | // Convert releases to an array. 18 | return dirs.split('\n') 19 | } 20 | 21 | /** 22 | * Test if all values are equal. 23 | * 24 | * @param {*[]} values 25 | * @returns {boolean} 26 | */ 27 | function equalValues(values) { 28 | return values.every(value => _.isEqual(value, values[0])) 29 | } 30 | 31 | /** 32 | * Compute the current release dir name. 33 | * 34 | * @param {object} result 35 | * @returns {string} 36 | */ 37 | function computeReleaseDirname(result) { 38 | if (!result.stdout) return null 39 | 40 | // Trim last breakline. 41 | const target = result.stdout.replace(/\n$/, '') 42 | return target.split(path.sep).pop() 43 | } 44 | 45 | function validateConfig(config) { 46 | const errors = [] 47 | if (!config.deployTo) { 48 | errors.push("Config must include a 'deployTo' property") 49 | } 50 | if (errors.length) { 51 | console.log(errors) 52 | throw new Error( 53 | 'Config is invalid. Please refer to errors above and try again.', 54 | ) 55 | } 56 | } 57 | 58 | function extendShipit(shipit) { 59 | /* eslint-disable no-param-reassign */ 60 | validateConfig(shipit.config) 61 | shipit.currentPath = path.join(shipit.config.deployTo, 'current') 62 | shipit.releasesPath = path.join(shipit.config.deployTo, 'releases') 63 | const config = { 64 | branch: 'master', 65 | keepReleases: 5, 66 | shallowClone: true, 67 | keepWorkspace: false, 68 | gitLogFormat: '%h: %s - %an', 69 | ...shipit.config, 70 | } 71 | Object.assign(shipit.config, config) 72 | /* eslint-enable no-param-reassign */ 73 | 74 | const Shipit = shipit.constructor 75 | 76 | /** 77 | * Return the current release dirname. 78 | */ 79 | Shipit.prototype.getCurrentReleaseDirname = async function getCurrentReleaseDirname() { 80 | const results = 81 | (await this.remote( 82 | util.format( 83 | 'if [ -h %s ]; then readlink %s; fi', 84 | this.currentPath, 85 | this.currentPath, 86 | ), 87 | )) || [] 88 | 89 | const releaseDirnames = results.map(computeReleaseDirname) 90 | 91 | if (!equalValues(releaseDirnames)) { 92 | throw new Error('Remote servers are not synced.') 93 | } 94 | 95 | if (!releaseDirnames[0]) { 96 | this.log('No current release found.') 97 | return null 98 | } 99 | 100 | return releaseDirnames[0] 101 | } 102 | 103 | /** 104 | * Return all remote releases (newest first) 105 | */ 106 | Shipit.prototype.getReleases = async function getReleases() { 107 | const results = await this.remote(`ls -r1 ${this.releasesPath}`) 108 | const releases = results.map(computeReleases) 109 | 110 | if (!equalValues(releases)) { 111 | throw new Error('Remote servers are not synced.') 112 | } 113 | 114 | return releases[0] 115 | } 116 | 117 | /** 118 | * Return SHA from remote REVISION file. 119 | * 120 | * @param {string} releaseDir Directory name of the relesase dir (YYYYMMDDHHmmss). 121 | */ 122 | Shipit.prototype.getRevision = async function getRevision(releaseDir) { 123 | const file = path.join(this.releasesPath, releaseDir, 'REVISION') 124 | const response = await this.remote( 125 | `if [ -f ${file} ]; then cat ${file} 2>/dev/null; fi;`, 126 | ) 127 | return response[0].stdout.trim() 128 | } 129 | 130 | Shipit.prototype.getPendingCommits = async function getPendingCommits() { 131 | const currentReleaseDirname = await this.getCurrentReleaseDirname() 132 | if (!currentReleaseDirname) return null 133 | 134 | const deployedRevision = await this.getRevision(currentReleaseDirname) 135 | if (!deployedRevision) return null 136 | 137 | const res = await this.local('git remote', { cwd: this.config.workspace }) 138 | const remotes = res && res.stdout ? res.stdout.split(/\s/) : [] 139 | if (remotes.length < 1) return null 140 | 141 | // Compare against currently undeployed revision 142 | const compareRevision = `${remotes[0]}/${this.config.branch}` 143 | 144 | const response = await this.local( 145 | `git log --pretty=format:"${shipit.config.gitLogFormat}" ${deployedRevision}..${compareRevision}`, 146 | { cwd: shipit.workspace }, 147 | ) 148 | const commits = response.stdout.trim() 149 | return commits || null 150 | } 151 | } 152 | 153 | export default extendShipit 154 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/index.js: -------------------------------------------------------------------------------- 1 | import deploy from './tasks/deploy' 2 | import rollback from './tasks/rollback' 3 | import pending from './tasks/pending' 4 | 5 | module.exports = shipit => { 6 | deploy(shipit) 7 | rollback(shipit) 8 | pending(shipit) 9 | } 10 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/clean.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-template */ 2 | import utils from 'shipit-utils' 3 | import extendShipit from '../../extendShipit' 4 | 5 | /** 6 | * Clean task. 7 | * - Remove old releases. 8 | */ 9 | const cleanTask = shipit => { 10 | utils.registerTask(shipit, 'deploy:clean', async () => { 11 | extendShipit(shipit) 12 | 13 | shipit.log( 14 | 'Keeping "%d" last releases, cleaning others', 15 | shipit.config.keepReleases, 16 | ) 17 | 18 | const command = 19 | '(ls -rd ' + 20 | shipit.releasesPath + 21 | '/*|head -n ' + 22 | shipit.config.keepReleases + 23 | ';ls -d ' + 24 | shipit.releasesPath + 25 | '/*)|sort|uniq -u|xargs rm -rf' 26 | await shipit.remote(command) 27 | 28 | shipit.emit('cleaned') 29 | }) 30 | } 31 | 32 | export default cleanTask 33 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/clean.test.js: -------------------------------------------------------------------------------- 1 | import Shipit from 'shipit-cli' 2 | import { start } from '../../../tests/util' 3 | import cleanTask from './clean' 4 | 5 | describe('deploy:clean task', () => { 6 | let shipit 7 | 8 | beforeEach(() => { 9 | shipit = new Shipit({ 10 | environment: 'test', 11 | log: jest.fn(), 12 | }) 13 | 14 | cleanTask(shipit) 15 | 16 | // Shipit config. 17 | shipit.initConfig({ 18 | test: { 19 | deployTo: '/remote/deploy', 20 | keepReleases: 5, 21 | }, 22 | }) 23 | 24 | shipit.remote = jest.fn(async () => []) 25 | }) 26 | 27 | it('should remove old releases', async () => { 28 | await start(shipit, 'deploy:clean') 29 | /* eslint-disable prefer-template */ 30 | expect(shipit.remote).toBeCalledWith( 31 | '(ls -rd /remote/deploy/releases/*|head -n 5;ls -d /remote/deploy/releases/*)|sort|uniq -u|xargs rm -rf', 32 | ) 33 | /* eslint-enable prefer-template */ 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/fetch.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import utils from 'shipit-utils' 4 | import chalk from 'chalk' 5 | import tmp from 'tmp-promise' 6 | import extendShipit from '../../extendShipit' 7 | 8 | /** 9 | * Fetch task. 10 | * - Create workspace. 11 | * - Fetch repository. 12 | * - Checkout commit-ish. 13 | */ 14 | const fetchTask = shipit => { 15 | utils.registerTask(shipit, 'deploy:fetch', async () => { 16 | extendShipit(shipit) 17 | 18 | /** 19 | * Create workspace. 20 | */ 21 | async function setupWorkspace() { 22 | const { keepWorkspace, workspace, shallowClone } = shipit.config 23 | 24 | shipit.log('Setup workspace...') 25 | if (workspace) { 26 | // eslint-disable-next-line no-param-reassign 27 | shipit.workspace = workspace 28 | } 29 | 30 | if (shallowClone) { 31 | const tmpDir = await tmp.dir({ mode: '0755' }) 32 | // eslint-disable-next-line no-param-reassign 33 | shipit.workspace = tmpDir.path 34 | 35 | if (workspace) { 36 | shipit.log( 37 | chalk.yellow( 38 | `Warning: Workspace path from config ("${workspace}") is being ignored, when shallowClone: true`, 39 | ), 40 | ) 41 | } 42 | 43 | shipit.log(`Temporary workspace created: "${shipit.workspace}"`) 44 | } 45 | 46 | if (!shipit.workspace || !fs.existsSync(shipit.workspace)) { 47 | throw new Error( 48 | `Workspace dir is required. Current value is: ${shipit.workspace}`, 49 | ) 50 | } 51 | 52 | if (!keepWorkspace && path.resolve(shipit.workspace) === process.cwd()) { 53 | throw new Error( 54 | 'Workspace should be a temporary directory. To use current working directory set keepWorkspace: true', 55 | ) 56 | } 57 | 58 | shipit.log(chalk.green('Workspace ready.')) 59 | } 60 | 61 | /** 62 | * Initialize repository. 63 | */ 64 | async function initRepository() { 65 | shipit.log('Initialize local repository in "%s"', shipit.workspace) 66 | await shipit.local('git init', { cwd: shipit.workspace }) 67 | shipit.log(chalk.green('Repository initialized.')) 68 | } 69 | 70 | /** 71 | * Set git config. 72 | */ 73 | async function setGitConfig() { 74 | if (!shipit.config.gitConfig) return 75 | 76 | shipit.log('Set custom git config options for "%s"', shipit.workspace) 77 | 78 | await Promise.all( 79 | Object.keys(shipit.config.gitConfig || {}).map(key => 80 | shipit.local(`git config ${key} "${shipit.config.gitConfig[key]}"`, { 81 | cwd: shipit.workspace, 82 | }), 83 | ), 84 | ) 85 | shipit.log(chalk.green('Git config set.')) 86 | } 87 | 88 | /** 89 | * Add remote. 90 | */ 91 | async function addRemote() { 92 | shipit.log('List local remotes.') 93 | 94 | const res = await shipit.local('git remote', { 95 | cwd: shipit.workspace, 96 | }) 97 | 98 | const remotes = res.stdout ? res.stdout.split(/\s/) : [] 99 | const method = remotes.indexOf('shipit') !== -1 ? 'set-url' : 'add' 100 | 101 | shipit.log( 102 | 'Update remote "%s" to local repository "%s"', 103 | shipit.config.repositoryUrl, 104 | shipit.workspace, 105 | ) 106 | 107 | // Update remote. 108 | await shipit.local( 109 | `git remote ${method} shipit ${shipit.config.repositoryUrl}`, 110 | { cwd: shipit.workspace }, 111 | ) 112 | 113 | shipit.log(chalk.green('Remote updated.')) 114 | } 115 | 116 | /** 117 | * Fetch repository. 118 | */ 119 | async function fetch() { 120 | let fetchCommand = 'git fetch shipit --prune' 121 | const fetchDepth = shipit.config.shallowClone ? ' --depth=1' : '' 122 | 123 | // fetch branches and tags separate to be compatible with git versions < 1.9 124 | fetchCommand += `${fetchDepth} && ${fetchCommand} "refs/tags/*:refs/tags/*"` 125 | 126 | shipit.log('Fetching repository "%s"', shipit.config.repositoryUrl) 127 | 128 | await shipit.local(fetchCommand, { cwd: shipit.workspace }) 129 | shipit.log(chalk.green('Repository fetched.')) 130 | } 131 | 132 | /** 133 | * Checkout commit-ish. 134 | */ 135 | async function checkout() { 136 | shipit.log('Checking out commit-ish "%s"', shipit.config.branch) 137 | await shipit.local(`git checkout ${shipit.config.branch}`, { 138 | cwd: shipit.workspace, 139 | }) 140 | shipit.log(chalk.green('Checked out.')) 141 | } 142 | 143 | /** 144 | * Hard reset of working tree. 145 | */ 146 | async function reset() { 147 | shipit.log('Resetting the working tree') 148 | await shipit.local('git reset --hard HEAD', { 149 | cwd: shipit.workspace, 150 | }) 151 | shipit.log(chalk.green('Reset working tree.')) 152 | } 153 | 154 | /** 155 | * Merge branch. 156 | */ 157 | async function merge() { 158 | shipit.log('Testing if commit-ish is a branch.') 159 | 160 | const res = await shipit.local( 161 | `git branch --list ${shipit.config.branch}`, 162 | { 163 | cwd: shipit.workspace, 164 | }, 165 | ) 166 | 167 | const isBranch = !!res.stdout 168 | 169 | if (!isBranch) { 170 | shipit.log(chalk.green('No branch, no merge.')) 171 | return 172 | } 173 | 174 | shipit.log('Commit-ish is a branch, merging...') 175 | 176 | // Merge branch. 177 | await shipit.local(`git merge shipit/${shipit.config.branch}`, { 178 | cwd: shipit.workspace, 179 | }) 180 | 181 | shipit.log(chalk.green('Branch merged.')) 182 | } 183 | 184 | /** 185 | * update submodules 186 | */ 187 | async function updateSubmodules() { 188 | if (!shipit.config.updateSubmodules) return 189 | 190 | shipit.log('Updating submodules.') 191 | await shipit.local('git submodule update --init --recursive', { 192 | cwd: shipit.workspace, 193 | }) 194 | shipit.log(chalk.green('Submodules updated')) 195 | } 196 | 197 | await setupWorkspace() 198 | 199 | if (shipit.config.repositoryUrl) { 200 | await initRepository() 201 | await setGitConfig() 202 | await addRemote() 203 | await fetch() 204 | await checkout() 205 | await reset() 206 | await merge() 207 | await updateSubmodules() 208 | } else { 209 | shipit.log(chalk.yellow('Skip fetching repo. No repositoryUrl provided')) 210 | } 211 | 212 | shipit.emit('fetched') 213 | }) 214 | } 215 | 216 | export default fetchTask 217 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/fetch.test.js: -------------------------------------------------------------------------------- 1 | import Shipit from 'shipit-cli' 2 | import fs from 'fs' 3 | import tmp from 'tmp-promise' 4 | import { start } from '../../../tests/util' 5 | import fetchTask from './fetch' 6 | 7 | jest.mock('tmp-promise', () => ({ 8 | dir: jest.fn(), 9 | })) 10 | jest.mock('fs') 11 | 12 | describe('deploy:fetch task', () => { 13 | let shipit 14 | let log 15 | 16 | beforeEach(() => { 17 | log = jest.fn() 18 | shipit = new Shipit({ 19 | environment: 'test', 20 | log, 21 | }) 22 | 23 | fetchTask(shipit) 24 | 25 | // Shipit config 26 | shipit.initConfig({ 27 | test: { 28 | deployTo: '/var/apps/dep', 29 | shallowClone: false, 30 | workspace: '/tmp/workspace', 31 | repositoryUrl: 'git://website.com/user/repo', 32 | }, 33 | }) 34 | 35 | shipit.local = jest.fn(async () => ({ stdout: 'ok' })) 36 | 37 | fs.existsSync.mockImplementation(() => true) 38 | tmp.dir.mockImplementation(() => 39 | Promise.resolve({ 40 | path: '/tmp/workspace-generated', 41 | }), 42 | ) 43 | }) 44 | 45 | it('should throw an error if workspace is current directory', async () => { 46 | jest.spyOn(process, 'cwd').mockImplementation(() => '/tmp/workspace') 47 | expect.assertions(1) 48 | 49 | await expect(start(shipit, 'deploy:fetch')).rejects.toMatchInlineSnapshot( 50 | `[Error: Workspace should be a temporary directory. To use current working directory set keepWorkspace: true]`, 51 | ) 52 | 53 | process.cwd.mockRestore() 54 | }) 55 | 56 | describe('setup workspace', () => { 57 | it('should use config.workspace if any', async () => { 58 | await start(shipit, 'deploy:fetch') 59 | 60 | expect(shipit.workspace).toBe('/tmp/workspace') 61 | }) 62 | 63 | it('should throw if workspace dir does not exists', async () => { 64 | fs.existsSync.mockImplementation(() => false) 65 | 66 | await expect(start(shipit, 'deploy:fetch')).rejects.toMatchInlineSnapshot( 67 | `[Error: Workspace dir is required. Current value is: /tmp/workspace]`, 68 | ) 69 | }) 70 | 71 | it('should create temp dir if shallowClone: true', async () => { 72 | shipit.config.shallowClone = true 73 | 74 | await start(shipit, 'deploy:fetch') 75 | 76 | expect(tmp.dir).toHaveBeenCalledWith({ mode: '0755' }) 77 | expect(shipit.workspace).toBe('/tmp/workspace-generated') 78 | }) 79 | 80 | it('should throw if workspace dir was not configured', async () => { 81 | delete shipit.config.workspace 82 | 83 | await expect(start(shipit, 'deploy:fetch')).rejects.toMatchInlineSnapshot( 84 | `[Error: Workspace dir is required. Current value is: undefined]`, 85 | ) 86 | }) 87 | }) 88 | 89 | describe('fetch repo', () => { 90 | it('should create repo, checkout and call sync', async () => { 91 | await start(shipit, 'deploy:fetch') 92 | 93 | const opts = { cwd: '/tmp/workspace' } 94 | 95 | expect(shipit.local).toBeCalledWith('git init', opts) 96 | expect(shipit.local).toBeCalledWith('git remote', opts) 97 | expect(shipit.local).toBeCalledWith( 98 | 'git remote add shipit git://website.com/user/repo', 99 | opts, 100 | ) 101 | expect(shipit.local).toBeCalledWith( 102 | 'git fetch shipit --prune && git fetch shipit --prune "refs/tags/*:refs/tags/*"', 103 | opts, 104 | ) 105 | expect(shipit.local).toBeCalledWith('git checkout master', opts) 106 | expect(shipit.local).toBeCalledWith('git branch --list master', opts) 107 | }) 108 | 109 | it('should create repo, checkout shallow and call sync', async () => { 110 | shipit.config.shallowClone = true 111 | delete shipit.config.workspace 112 | 113 | await start(shipit, 'deploy:fetch') 114 | 115 | const opts = { cwd: '/tmp/workspace-generated' } 116 | 117 | expect(shipit.local).toBeCalledWith('git init', opts) 118 | expect(shipit.local).toBeCalledWith('git remote', opts) 119 | expect(shipit.local).toBeCalledWith( 120 | 'git remote add shipit git://website.com/user/repo', 121 | opts, 122 | ) 123 | expect(shipit.local).toBeCalledWith( 124 | 'git fetch shipit --prune --depth=1 && git fetch shipit --prune "refs/tags/*:refs/tags/*"', 125 | opts, 126 | ) 127 | expect(shipit.local).toBeCalledWith('git checkout master', opts) 128 | expect(shipit.local).toBeCalledWith('git branch --list master', opts) 129 | }) 130 | 131 | it('should create repo, checkout and call sync, update submodules', async () => { 132 | shipit.config.updateSubmodules = true 133 | 134 | await start(shipit, 'deploy:fetch') 135 | 136 | const opts = { cwd: '/tmp/workspace' } 137 | 138 | expect(shipit.local).toBeCalledWith('git init', opts) 139 | expect(shipit.local).toBeCalledWith('git remote', opts) 140 | expect(shipit.local).toBeCalledWith( 141 | 'git remote add shipit git://website.com/user/repo', 142 | opts, 143 | ) 144 | expect(shipit.local).toBeCalledWith( 145 | 'git fetch shipit --prune && git fetch shipit --prune "refs/tags/*:refs/tags/*"', 146 | opts, 147 | ) 148 | expect(shipit.local).toBeCalledWith('git checkout master', opts) 149 | expect(shipit.local).toBeCalledWith('git branch --list master', opts) 150 | expect(shipit.local).toBeCalledWith( 151 | 'git submodule update --init --recursive', 152 | opts, 153 | ) 154 | }) 155 | 156 | it('should create repo, set repo config, checkout and call sync', async () => { 157 | shipit.config.gitConfig = { 158 | foo: 'bar', 159 | baz: 'quux', 160 | } 161 | 162 | await start(shipit, 'deploy:fetch') 163 | 164 | const opts = { cwd: '/tmp/workspace' } 165 | 166 | expect(shipit.local).toBeCalledWith('git init', opts) 167 | expect(shipit.local).toBeCalledWith('git config foo "bar"', opts) 168 | expect(shipit.local).toBeCalledWith('git config baz "quux"', opts) 169 | expect(shipit.local).toBeCalledWith('git remote', opts) 170 | expect(shipit.local).toBeCalledWith( 171 | 'git remote add shipit git://website.com/user/repo', 172 | opts, 173 | ) 174 | expect(shipit.local).toBeCalledWith( 175 | 'git fetch shipit --prune && git fetch shipit --prune "refs/tags/*:refs/tags/*"', 176 | opts, 177 | ) 178 | expect(shipit.local).toBeCalledWith('git checkout master', opts) 179 | expect(shipit.local).toBeCalledWith('git branch --list master', opts) 180 | }) 181 | 182 | it('should skip fetching if no repositoryUrl provided', async () => { 183 | delete shipit.config.repositoryUrl 184 | 185 | await start(shipit, 'deploy:fetch') 186 | 187 | expect(shipit.local).not.toHaveBeenCalled() 188 | expect(log).toBeCalledWith( 189 | expect.stringContaining( 190 | 'Skip fetching repo. No repositoryUrl provided', 191 | ), 192 | ) 193 | }) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/finish.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import extendShipit from '../../extendShipit' 3 | 4 | /** 5 | * Finish task. 6 | * - Emit an event "deployed". 7 | */ 8 | 9 | const finishTask = shipit => { 10 | utils.registerTask(shipit, 'deploy:finish', () => { 11 | extendShipit(shipit) 12 | shipit.emit('deployed') 13 | }) 14 | } 15 | 16 | export default finishTask 17 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/finish.test.js: -------------------------------------------------------------------------------- 1 | import Shipit from 'shipit-cli' 2 | import { start } from '../../../tests/util' 3 | import finishTask from './finish' 4 | 5 | describe('deploy:finish task', () => { 6 | let shipit 7 | 8 | beforeEach(() => { 9 | shipit = new Shipit({ 10 | environment: 'test', 11 | log: jest.fn(), 12 | }) 13 | 14 | finishTask(shipit) 15 | 16 | // Shipit config. 17 | shipit.initConfig({ 18 | test: { 19 | deployTo: '/', 20 | }, 21 | }) 22 | shipit.releasesPath = '/remote/deploy/releases' 23 | }) 24 | 25 | it('should emit a "deployed" event', async () => { 26 | const spy = jest.fn() 27 | shipit.on('deployed', spy) 28 | expect(spy).toHaveBeenCalledTimes(0) 29 | await start(shipit, 'deploy:finish') 30 | expect(spy).toHaveBeenCalledTimes(1) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/index.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import init from './init' 3 | import fetch from './fetch' 4 | import update from './update' 5 | import publish from './publish' 6 | import clean from './clean' 7 | import finish from './finish' 8 | 9 | /** 10 | * Deploy task. 11 | * - deploy:fetch 12 | * - deploy:update 13 | * - deploy:publish 14 | * - deploy:clean 15 | * - deploy:finish 16 | */ 17 | 18 | export default shipit => { 19 | init(shipit) 20 | fetch(shipit) 21 | update(shipit) 22 | publish(shipit) 23 | clean(shipit) 24 | finish(shipit) 25 | 26 | utils.registerTask(shipit, 'deploy', [ 27 | 'deploy:init', 28 | 'deploy:fetch', 29 | 'deploy:update', 30 | 'deploy:publish', 31 | 'deploy:clean', 32 | 'deploy:finish', 33 | ]) 34 | } 35 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/init.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import extendShipit from '../../extendShipit' 3 | 4 | /** 5 | * Init task. 6 | * - Emit deploy event. 7 | */ 8 | const initTask = shipit => { 9 | utils.registerTask(shipit, 'deploy:init', () => { 10 | extendShipit(shipit) 11 | shipit.emit('deploy') 12 | }) 13 | } 14 | 15 | export default initTask 16 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/init.test.js: -------------------------------------------------------------------------------- 1 | import Shipit from 'shipit-cli' 2 | import { start } from '../../../tests/util' 3 | import initTask from './init' 4 | 5 | describe('deploy:init task', () => { 6 | let shipit 7 | 8 | beforeEach(() => { 9 | shipit = new Shipit({ 10 | environment: 'test', 11 | log: jest.fn(), 12 | }) 13 | 14 | initTask(shipit) 15 | 16 | // Shipit config. 17 | shipit.initConfig({ 18 | test: { 19 | deployTo: '/', 20 | }, 21 | }) 22 | shipit.releasesPath = '/remote/deploy/releases' 23 | }) 24 | 25 | it('should emit a "deploy" event', async () => { 26 | const spy = jest.fn() 27 | shipit.on('deploy', spy) 28 | expect(spy).toHaveBeenCalledTimes(0) 29 | await start(shipit, 'deploy:init') 30 | expect(spy).toHaveBeenCalledTimes(1) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/publish.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import chalk from 'chalk' 3 | import path from 'path2/posix' 4 | import extendShipit from '../../extendShipit' 5 | 6 | /** 7 | * Publish task. 8 | * - Update symbolic link. 9 | */ 10 | const publishTask = shipit => { 11 | utils.registerTask(shipit, 'deploy:publish', async () => { 12 | extendShipit(shipit) 13 | 14 | shipit.log('Publishing release "%s"', shipit.releasePath) 15 | 16 | const relativeReleasePath = path.join('releases', shipit.releaseDirname) 17 | 18 | /* eslint-disable prefer-template */ 19 | const res = await shipit.remote( 20 | 'cd ' + 21 | shipit.config.deployTo + 22 | ' && ' + 23 | 'if [ -d current ] && [ ! -L current ]; then ' + 24 | 'echo "ERR: could not make symlink"; ' + 25 | 'else ' + 26 | 'ln -nfs ' + 27 | relativeReleasePath + 28 | ' current_tmp && ' + 29 | 'mv -fT current_tmp current; ' + 30 | 'fi', 31 | ) 32 | /* eslint-enable prefer-template */ 33 | 34 | const failedresult = 35 | res && res.stdout 36 | ? res.stdout.filter(r => r.indexOf('could not make symlink') > -1) 37 | : [] 38 | if (failedresult.length && failedresult.length > 0) { 39 | shipit.log( 40 | chalk.yellow( 41 | `Symbolic link at remote not made, as something already exists at ${path( 42 | shipit.config.deployTo, 43 | 'current', 44 | )}`, 45 | ), 46 | ) 47 | } 48 | 49 | shipit.log(chalk.green('Release published.')) 50 | 51 | shipit.emit('published') 52 | }) 53 | } 54 | 55 | export default publishTask 56 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/publish.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path2/posix' 2 | import Shipit from 'shipit-cli' 3 | import { start } from '../../../tests/util' 4 | import publishTask from './publish' 5 | 6 | describe('deploy:publish task', () => { 7 | let shipit 8 | 9 | beforeEach(() => { 10 | shipit = new Shipit({ 11 | environment: 'test', 12 | log: jest.fn(), 13 | }) 14 | 15 | publishTask(shipit) 16 | 17 | // Shipit config 18 | shipit.initConfig({ 19 | test: { 20 | deployTo: '/remote/deploy', 21 | }, 22 | }) 23 | 24 | shipit.releasePath = '/remote/deploy/releases/20141704123138' 25 | shipit.releaseDirname = '20141704123138' 26 | shipit.currentPath = path.join(shipit.config.deployTo, 'current') 27 | shipit.releasesPath = path.join(shipit.config.deployTo, 'releases') 28 | 29 | shipit.remote = jest.fn(async () => []) 30 | }) 31 | 32 | it('should update the symbolic link', async () => { 33 | await start(shipit, 'deploy:publish') 34 | expect(shipit.currentPath).toBe('/remote/deploy/current') 35 | expect(shipit.remote).toBeCalledWith( 36 | 'cd /remote/deploy && if [ -d current ] && [ ! -L current ]; then echo "ERR: could not make symlink"; else ln -nfs releases/20141704123138 current_tmp && mv -fT current_tmp current; fi', 37 | ) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/update.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import path from 'path2/posix' 3 | import p from 'path'; 4 | import moment from 'moment' 5 | import chalk from 'chalk' 6 | import util from 'util' 7 | import rmfr from 'rmfr' 8 | import _ from 'lodash' 9 | import extendShipit from '../../extendShipit' 10 | 11 | /** 12 | * Update task. 13 | * - Set previous release. 14 | * - Set previous revision. 15 | * - Create and define release path. 16 | * - Copy previous release (for faster rsync) 17 | * - Set current revision and write REVISION file. 18 | * - Remote copy project. 19 | * - Remove workspace. 20 | */ 21 | const updateTask = shipit => { 22 | utils.registerTask(shipit, 'deploy:update', async () => { 23 | extendShipit(shipit) 24 | 25 | /** 26 | * Copy previous release to release dir. 27 | */ 28 | 29 | async function copyPreviousRelease() { 30 | const copyParameter = shipit.config.copy || '-a' 31 | if (!shipit.previousRelease || shipit.config.copy === false) return 32 | shipit.log('Copy previous release to "%s"', shipit.releasePath) 33 | await shipit.remote( 34 | util.format( 35 | 'cp %s %s/. %s', 36 | copyParameter, 37 | path.join(shipit.releasesPath, shipit.previousRelease), 38 | shipit.releasePath, 39 | ), 40 | ) 41 | } 42 | 43 | /** 44 | * Create and define release path. 45 | */ 46 | async function createReleasePath() { 47 | /* eslint-disable no-param-reassign */ 48 | shipit.releaseDirname = moment.utc().format('YYYYMMDDHHmmss') 49 | shipit.releasePath = path.join(shipit.releasesPath, shipit.releaseDirname) 50 | /* eslint-enable no-param-reassign */ 51 | 52 | shipit.log('Create release path "%s"', shipit.releasePath) 53 | await shipit.remote(`mkdir -p ${shipit.releasePath}`) 54 | shipit.log(chalk.green('Release path created.')) 55 | } 56 | 57 | /** 58 | * Remote copy project. 59 | */ 60 | 61 | async function remoteCopy() { 62 | const options = _.get(shipit.config, 'deploy.remoteCopy') || { 63 | rsync: '--del', 64 | } 65 | const rsyncFrom = shipit.config.rsyncFrom || shipit.workspace 66 | const uploadDirPath = p.resolve(rsyncFrom, shipit.config.dirToCopy || ''); 67 | 68 | shipit.log('Copy project to remote servers.') 69 | 70 | let srcDirectory = `${uploadDirPath}/`; 71 | if(options.copyAsDir){ 72 | srcDirectory = srcDirectory.slice(0, -1); 73 | } 74 | await shipit.remoteCopy(srcDirectory, shipit.releasePath, options) 75 | shipit.log(chalk.green('Finished copy.')) 76 | } 77 | 78 | /** 79 | * Set shipit.previousRevision from remote REVISION file. 80 | */ 81 | async function setPreviousRevision() { 82 | /* eslint-disable no-param-reassign */ 83 | shipit.previousRevision = null 84 | /* eslint-enable no-param-reassign */ 85 | 86 | if (!shipit.previousRelease) return 87 | 88 | const revision = await shipit.getRevision(shipit.previousRelease) 89 | if (revision) { 90 | shipit.log(chalk.green('Previous revision found.')) 91 | /* eslint-disable no-param-reassign */ 92 | shipit.previousRevision = revision 93 | /* eslint-enable no-param-reassign */ 94 | } 95 | } 96 | 97 | /** 98 | * Set shipit.previousRelease. 99 | */ 100 | async function setPreviousRelease() { 101 | /* eslint-disable no-param-reassign */ 102 | shipit.previousRelease = null 103 | /* eslint-enable no-param-reassign */ 104 | const currentReleaseDirname = await shipit.getCurrentReleaseDirname() 105 | if (currentReleaseDirname) { 106 | shipit.log(chalk.green('Previous release found.')) 107 | /* eslint-disable no-param-reassign */ 108 | shipit.previousRelease = currentReleaseDirname 109 | /* eslint-enable no-param-reassign */ 110 | } 111 | } 112 | 113 | /** 114 | * Set shipit.currentRevision and write it to REVISION file. 115 | */ 116 | async function setCurrentRevision() { 117 | shipit.log('Setting current revision and creating revision file.') 118 | 119 | const response = await shipit.local( 120 | `git rev-parse ${shipit.config.branch}`, 121 | { 122 | cwd: shipit.workspace, 123 | }, 124 | ) 125 | 126 | /* eslint-disable no-param-reassign */ 127 | shipit.currentRevision = response.stdout.trim() 128 | /* eslint-enable no-param-reassign */ 129 | 130 | await shipit.remote( 131 | `echo "${shipit.currentRevision}" > ${path.join( 132 | shipit.releasePath, 133 | 'REVISION', 134 | )}`, 135 | ) 136 | shipit.log(chalk.green('Revision file created.')) 137 | } 138 | 139 | async function removeWorkspace() { 140 | if (!shipit.config.keepWorkspace && shipit.config.shallowClone) { 141 | shipit.log(`Removing workspace "${shipit.workspace}"`) 142 | await rmfr(shipit.workspace) 143 | shipit.log(chalk.green('Workspace removed.')) 144 | } 145 | } 146 | 147 | await setPreviousRelease() 148 | await setPreviousRevision() 149 | await createReleasePath() 150 | await copyPreviousRelease() 151 | await remoteCopy() 152 | await setCurrentRevision() 153 | await removeWorkspace() 154 | shipit.emit('updated') 155 | }) 156 | } 157 | 158 | export default updateTask 159 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/deploy/update.test.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import _ from 'lodash' 3 | import rmfr from 'rmfr' 4 | import path from 'path2/posix' 5 | import Shipit from 'shipit-cli' 6 | import { start } from '../../../tests/util' 7 | import updateTask from './update' 8 | 9 | jest.mock('rmfr') 10 | 11 | function createShipitInstance(config) { 12 | const shipit = new Shipit({ 13 | environment: 'test', 14 | log: jest.fn(), 15 | }) 16 | 17 | updateTask(shipit) 18 | 19 | // Shipit config 20 | shipit.initConfig({ 21 | test: _.merge( 22 | { 23 | workspace: '/tmp/workspace', 24 | deployTo: '/remote/deploy', 25 | }, 26 | config, 27 | ), 28 | }) 29 | 30 | shipit.workspace = '/tmp/workspace' 31 | shipit.currentPath = path.join(shipit.config.deployTo, 'current') 32 | shipit.releasesPath = path.join(shipit.config.deployTo, 'releases') 33 | 34 | return shipit 35 | } 36 | 37 | function stubShipit(shipit) { 38 | /* eslint-disable no-param-reassign */ 39 | shipit.remote = jest.fn(async () => []) 40 | shipit.remoteCopy = jest.fn(async () => []) 41 | shipit.local = jest.fn(async command => { 42 | switch (command) { 43 | case `git rev-parse ${shipit.config.branch}`: 44 | return { stdout: '9d63d434a921f496c12854a53cef8d293e2b4756\n' } 45 | default: 46 | return {} 47 | } 48 | }) 49 | /* eslint-enable no-param-reassign */ 50 | } 51 | 52 | describe('deploy:update task', () => { 53 | let shipit 54 | 55 | beforeEach(() => { 56 | shipit = createShipitInstance() 57 | moment.utc = () => ({ 58 | format: jest.fn(format => format), 59 | }) 60 | }) 61 | 62 | describe('update release', () => { 63 | beforeEach(() => { 64 | stubShipit(shipit) 65 | }) 66 | 67 | it('should create release path, and do a remote copy', async () => { 68 | await start(shipit, 'deploy:update') 69 | expect(shipit.releaseDirname).toBe('YYYYMMDDHHmmss') 70 | expect(shipit.releasesPath).toBe('/remote/deploy/releases') 71 | expect(shipit.releasePath).toBe('/remote/deploy/releases/YYYYMMDDHHmmss') 72 | expect(shipit.remote).toBeCalledWith( 73 | 'mkdir -p /remote/deploy/releases/YYYYMMDDHHmmss', 74 | ) 75 | expect(shipit.remoteCopy).toBeCalledWith( 76 | '/tmp/workspace/', 77 | '/remote/deploy/releases/YYYYMMDDHHmmss', 78 | { rsync: '--del' }, 79 | ) 80 | }) 81 | 82 | describe('dirToCopy option', () => { 83 | it('should correct join relative path', () => { 84 | const paths = [ 85 | { res: '/tmp/workspace/build/', dirToCopy: 'build' }, 86 | { res: '/tmp/workspace/build/', dirToCopy: './build' }, 87 | { res: '/tmp/workspace/build/', dirToCopy: './build/' }, 88 | { res: '/tmp/workspace/build/', dirToCopy: 'build/.' }, 89 | { res: '/tmp/workspace/build/src/', dirToCopy: 'build/src' }, 90 | { res: '/tmp/workspace/build/src/', dirToCopy: 'build/src' }, 91 | ] 92 | return Promise.all( 93 | paths.map(async p => { 94 | const sh = createShipitInstance({ 95 | dirToCopy: p.dirToCopy, 96 | }) 97 | stubShipit(sh) 98 | await start(sh, 'deploy:update') 99 | expect(sh.remoteCopy).toBeCalledWith( 100 | p.res, 101 | '/remote/deploy/releases/YYYYMMDDHHmmss', 102 | { 103 | rsync: '--del', 104 | }, 105 | ) 106 | }), 107 | ) 108 | }) 109 | }) 110 | 111 | describe('remoteCopy option', () => { 112 | it('should accept rsync options', async () => { 113 | const sh = createShipitInstance({ 114 | deploy: { remoteCopy: { rsync: '--foo' } }, 115 | }) 116 | stubShipit(sh) 117 | 118 | await start(sh, 'deploy:update') 119 | 120 | expect(sh.remoteCopy).toBeCalledWith( 121 | '/tmp/workspace/', 122 | '/remote/deploy/releases/YYYYMMDDHHmmss', 123 | { rsync: '--foo' }, 124 | ) 125 | }) 126 | }) 127 | 128 | it('should accept rsync options', async () => { 129 | const sh = createShipitInstance({ 130 | deploy: { remoteCopy: { rsync: '--foo', copyAsDir: true } }, 131 | }) 132 | stubShipit(sh) 133 | 134 | await start(sh, 'deploy:update') 135 | 136 | expect(sh.remoteCopy).toBeCalledWith( 137 | '/tmp/workspace', 138 | '/remote/deploy/releases/YYYYMMDDHHmmss', 139 | { rsync: '--foo', copyAsDir: true }, 140 | ) 141 | }) 142 | }) 143 | 144 | describe('#setPreviousRevision', () => { 145 | beforeEach(() => { 146 | stubShipit(shipit) 147 | }) 148 | 149 | describe('no previous revision', () => { 150 | it('should set shipit.previousRevision to null', async () => { 151 | await start(shipit, 'deploy:update') 152 | 153 | expect(shipit.previousRevision).toBe(null) 154 | expect(shipit.local).toBeCalledWith( 155 | `git rev-parse ${shipit.config.branch}`, 156 | { cwd: '/tmp/workspace' }, 157 | ) 158 | }) 159 | }) 160 | }) 161 | 162 | describe('#setPreviousRelease', () => { 163 | beforeEach(() => { 164 | stubShipit(shipit) 165 | }) 166 | 167 | it('should set shipit.previousRelease to null when no previous release', async () => { 168 | await start(shipit, 'deploy:update') 169 | expect(shipit.previousRelease).toBe(null) 170 | }) 171 | 172 | it('should set shipit.previousRelease to (still) current release when one release exist', async () => { 173 | shipit.remote = jest.fn(async () => [{ stdout: '20141704123137\n' }]) 174 | await start(shipit, 'deploy:update') 175 | expect(shipit.previousRelease).toBe('20141704123137') 176 | }) 177 | }) 178 | 179 | describe('#copyPreviousRelease', () => { 180 | beforeEach(() => { 181 | stubShipit(shipit) 182 | }) 183 | 184 | describe('no previous release', () => { 185 | it('should proceed with rsync', async () => { 186 | await start(shipit, 'deploy:update') 187 | expect(shipit.previousRelease).toBe(null) 188 | }) 189 | }) 190 | }) 191 | 192 | describe('#setCurrentRevision', () => { 193 | beforeEach(() => { 194 | stubShipit(shipit) 195 | shipit.remote = jest.fn(async command => { 196 | if (/^if \[ -f/.test(command)) { 197 | return [{ stdout: '9d63d434a921f496c12854a53cef8d293e2b4756\n' }] 198 | } 199 | 200 | if ( 201 | command === 202 | 'if [ -h /remote/deploy/current ]; then readlink /remote/deploy/current; fi' 203 | ) { 204 | return [{ stdout: '/remote/deploy/releases/20141704123137' }] 205 | } 206 | 207 | if (command === 'ls -r1 /remote/deploy/releases') { 208 | return [ 209 | { stdout: '20141704123137\n20141704123133\n' }, 210 | { stdout: '20141704123137\n20141704123133\n' }, 211 | ] 212 | } 213 | 214 | if (/^cp/.test(command)) { 215 | const args = command.split(' ') 216 | if (/\/.$/.test(args[args.length - 2]) === false) { 217 | throw new Error('Copy folder contents, not the folder itself') 218 | } 219 | } 220 | 221 | return [{ stdout: '' }] 222 | }) 223 | }) 224 | 225 | it('should set shipit.currentRevision', async () => { 226 | await start(shipit, 'deploy:update') 227 | expect(shipit.currentRevision).toBe( 228 | '9d63d434a921f496c12854a53cef8d293e2b4756', 229 | ) 230 | }) 231 | 232 | it('should update remote REVISION file', async () => { 233 | await start(shipit, 'deploy:update') 234 | const revision = await shipit.getRevision('20141704123137') 235 | expect(revision).toBe('9d63d434a921f496c12854a53cef8d293e2b4756') 236 | }) 237 | 238 | it('should copy contents of previous release into new folder', async () => { 239 | await start(shipit, 'deploy:update') 240 | expect(shipit.previousRelease).not.toBe(null) 241 | }) 242 | }) 243 | 244 | it('should remove workspace when shallow cloning', async () => { 245 | shipit.config.shallowClone = true 246 | stubShipit(shipit) 247 | rmfr.mockClear() 248 | expect(rmfr).not.toHaveBeenCalled() 249 | await start(shipit, 'deploy:update') 250 | expect(rmfr).toHaveBeenCalledWith('/tmp/workspace') 251 | }) 252 | 253 | it('should keep workspace when not shallow cloning', async () => { 254 | shipit.config.shallowClone = false 255 | stubShipit(shipit) 256 | rmfr.mockClear() 257 | expect(rmfr).not.toHaveBeenCalled() 258 | await start(shipit, 'deploy:update') 259 | expect(rmfr).not.toHaveBeenCalledWith('/tmp/workspace') 260 | }) 261 | 262 | it('should keep workspace when keepWorkspace is true', async () => { 263 | shipit.config.shallowClone = true 264 | shipit.config.keepWorkspace = true 265 | 266 | stubShipit(shipit) 267 | 268 | rmfr.mockClear() 269 | 270 | await start(shipit, 'deploy:update') 271 | 272 | expect(rmfr).not.toHaveBeenCalled() 273 | }) 274 | }) 275 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/pending/index.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import logTask from './log' 3 | import fetchTask from '../deploy/fetch' 4 | 5 | /** 6 | * Pending task. 7 | * - deploy:fetch 8 | * - pending:log 9 | */ 10 | export default shipit => { 11 | logTask(shipit) 12 | fetchTask(shipit) 13 | utils.registerTask(shipit, 'pending', ['deploy:fetch', 'pending:log']) 14 | } 15 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/pending/log.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import chalk from 'chalk' 3 | import extendShipit from '../../extendShipit' 4 | 5 | /** 6 | * Log task. 7 | */ 8 | const logTask = shipit => { 9 | utils.registerTask(shipit, 'pending:log', async () => { 10 | extendShipit(shipit) 11 | const commits = await shipit.getPendingCommits() 12 | const msg = commits 13 | ? chalk.yellow(chalk.underline('\nPending commits:\n') + commits) 14 | : chalk.green('\nNo pending commits.') 15 | 16 | shipit.log(msg) 17 | }) 18 | } 19 | 20 | export default logTask 21 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/pending/log.test.js: -------------------------------------------------------------------------------- 1 | import Shipit from 'shipit-cli' 2 | import { start } from '../../../tests/util' 3 | import logTask from './log' 4 | 5 | describe('pending:log task', () => { 6 | let shipit 7 | 8 | beforeEach(() => { 9 | shipit = new Shipit({ 10 | environment: 'test', 11 | log: jest.fn(), 12 | }) 13 | 14 | logTask(shipit) 15 | 16 | // Shipit config 17 | shipit.initConfig({ 18 | test: { 19 | deployTo: '/remote/deploy', 20 | }, 21 | }) 22 | 23 | shipit.releasePath = '/remote/deploy/releases/20141704123138' 24 | shipit.releaseDirname = '20141704123138' 25 | 26 | shipit.remote = jest.fn(async () => []) 27 | }) 28 | 29 | describe('#getPendingCommits', () => { 30 | describe('no current release', () => { 31 | it('should return null', async () => { 32 | await start(shipit, 'pending:log') 33 | const commits = await shipit.getPendingCommits() 34 | expect(commits).toBe(null) 35 | }) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/rollback/finish.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import extendShipit from '../../extendShipit' 3 | 4 | /** 5 | * Update task. 6 | * - Emit an event "rollbacked". 7 | */ 8 | export default shipit => { 9 | utils.registerTask(shipit, 'rollback:finish', async () => { 10 | extendShipit(shipit) 11 | 12 | if (shipit.config.deleteOnRollback) { 13 | if (!shipit.prevReleaseDirname || !shipit.prevReleasePath) { 14 | throw new Error("Can't find release to delete") 15 | } 16 | 17 | const command = `rm -rf ${shipit.prevReleasePath}` 18 | await shipit.remote(command) 19 | } 20 | 21 | shipit.emit('rollbacked') 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/rollback/finish.test.js: -------------------------------------------------------------------------------- 1 | import Shipit from 'shipit-cli' 2 | import path from 'path2/posix' 3 | import { start } from '../../../tests/util' 4 | import finishTask from './finish' 5 | 6 | describe('rollback:finish task', () => { 7 | let shipit 8 | const readLinkCommand = 9 | 'if [ -h /remote/deploy/current ]; then readlink /remote/deploy/current; fi' 10 | 11 | beforeEach(() => { 12 | shipit = new Shipit({ 13 | environment: 'test', 14 | log: jest.fn(), 15 | }) 16 | 17 | finishTask(shipit) 18 | 19 | // Shipit config 20 | shipit.initConfig({ 21 | test: { 22 | deployTo: '/remote/deploy', 23 | deleteOnRollback: false, 24 | }, 25 | }) 26 | 27 | shipit.releasePath = '/remote/deploy/releases/20141704123137' 28 | shipit.releaseDirname = '20141704123137' 29 | shipit.currentPath = path.join(shipit.config.deployTo, 'current') 30 | shipit.releasesPath = path.join(shipit.config.deployTo, 'releases') 31 | shipit.rollbackDirName = '20141704123137' 32 | }) 33 | 34 | describe('delete rollbacked release', () => { 35 | beforeEach(() => { 36 | shipit.remote = jest.fn(async command => { 37 | switch (command) { 38 | case readLinkCommand: 39 | return [{ stdout: '/remote/deploy/releases/20141704123136\n' }] 40 | case 'ls -r1 /remote/deploy/releases': 41 | return [{ stdout: '20141704123137\n20141704123136\n' }] 42 | case 'rm -rf /remote/deploy/releases/20141704123137': 43 | default: 44 | return [] 45 | } 46 | }) 47 | shipit.config.deleteOnRollback = true 48 | }) 49 | 50 | it('undefined releases path', async () => { 51 | expect.assertions(1) 52 | try { 53 | await start(shipit, 'rollback:finish') 54 | } catch (err) { 55 | expect(err.message).toBe("Can't find release to delete") 56 | } 57 | }) 58 | 59 | it('undefined previous directory name', async () => { 60 | shipit.prevReleasePath = '/remote/deploy/releases/' 61 | expect.assertions(1) 62 | try { 63 | await start(shipit, 'rollback:finish') 64 | } catch (err) { 65 | expect(err.message).toBe("Can't find release to delete") 66 | } 67 | }) 68 | 69 | it('successful delete', async () => { 70 | // set up test specific variables 71 | shipit.prevReleaseDirname = '20141704123137' 72 | shipit.prevReleasePath = '/remote/deploy/releases/20141704123137' 73 | 74 | const spy = jest.fn() 75 | shipit.on('rollbacked', spy) 76 | await start(shipit, 'rollback:finish') 77 | expect(shipit.prevReleaseDirname).toBe('20141704123137') 78 | expect(shipit.remote).toBeCalledWith( 79 | 'rm -rf /remote/deploy/releases/20141704123137', 80 | ) 81 | expect(spy).toBeCalled() 82 | }) 83 | }) 84 | 85 | it('should emit an event', async () => { 86 | const spy = jest.fn() 87 | shipit.on('rollbacked', spy) 88 | await start(shipit, 'rollback:finish') 89 | expect(spy).toBeCalled() 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/rollback/index.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import initTask from './init' 3 | import fetchTask from '../deploy/fetch' 4 | import cleanTask from '../deploy/clean' 5 | import finishTask from './finish' 6 | 7 | /** 8 | * Rollback task. 9 | * - rollback:init 10 | * - deploy:publish 11 | * - deploy:clean 12 | */ 13 | export default shipit => { 14 | initTask(shipit) 15 | fetchTask(shipit) 16 | cleanTask(shipit) 17 | finishTask(shipit) 18 | 19 | utils.registerTask(shipit, 'rollback', [ 20 | 'rollback:init', 21 | 'deploy:publish', 22 | 'deploy:clean', 23 | 'rollback:finish', 24 | ]) 25 | } 26 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/rollback/init.js: -------------------------------------------------------------------------------- 1 | import utils from 'shipit-utils' 2 | import path from 'path2/posix' 3 | import extendShipit from '../../extendShipit' 4 | 5 | /** 6 | * Update task. 7 | * - Create and define release path. 8 | * - Remote copy project. 9 | */ 10 | export default shipit => { 11 | utils.registerTask(shipit, 'rollback:init', async () => { 12 | extendShipit(shipit) 13 | 14 | shipit.log('Get current release dirname.') 15 | 16 | const currentRelease = await shipit.getCurrentReleaseDirname() 17 | 18 | if (!currentRelease) { 19 | throw new Error('Cannot find current release dirname.') 20 | } 21 | 22 | shipit.log('Current release dirname : %s.', currentRelease) 23 | shipit.log('Getting dist releases.') 24 | 25 | const releases = await shipit.getReleases() 26 | 27 | if (!releases) { 28 | throw new Error('Cannot read releases.') 29 | } 30 | 31 | shipit.log('Dist releases : %j.', releases) 32 | 33 | const currentReleaseIndex = releases.indexOf(currentRelease) 34 | const rollbackReleaseIndex = currentReleaseIndex + 1 35 | 36 | /* eslint-disable no-param-reassign */ 37 | shipit.releaseDirname = releases[rollbackReleaseIndex] 38 | 39 | // Save the previous release in case we need to delete it later 40 | shipit.prevReleaseDirname = releases[currentReleaseIndex] 41 | shipit.prevReleasePath = path.join( 42 | shipit.releasesPath, 43 | shipit.prevReleaseDirname, 44 | ) 45 | 46 | shipit.log('Will rollback to %s.', shipit.releaseDirname) 47 | 48 | if (!shipit.releaseDirname) { 49 | throw new Error('Cannot rollback, release not found.') 50 | } 51 | 52 | shipit.releasePath = path.join(shipit.releasesPath, shipit.releaseDirname) 53 | /* eslint-enable no-param-reassign */ 54 | 55 | shipit.emit('rollback') 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /packages/shipit-deploy/src/tasks/rollback/init.test.js: -------------------------------------------------------------------------------- 1 | import Shipit from 'shipit-cli' 2 | import { start } from '../../../tests/util' 3 | import initTask from './init' 4 | 5 | describe('rollback:init task', () => { 6 | let shipit 7 | const readLinkCommand = 8 | 'if [ -h /remote/deploy/current ]; then readlink /remote/deploy/current; fi' 9 | 10 | beforeEach(() => { 11 | shipit = new Shipit({ 12 | environment: 'test', 13 | log: jest.fn(), 14 | }) 15 | 16 | initTask(shipit) 17 | 18 | // Shipit config 19 | shipit.initConfig({ 20 | test: { 21 | deployTo: '/remote/deploy', 22 | }, 23 | }) 24 | }) 25 | 26 | describe('#getCurrentReleaseDirName', () => { 27 | describe('unsync server', () => { 28 | beforeEach(() => { 29 | shipit.remote = jest.fn(async () => [ 30 | { stdout: '/remote/deploy/releases/20141704123138' }, 31 | { stdout: '/remote/deploy/releases/20141704123137' }, 32 | ]) 33 | }) 34 | 35 | it('should return an error', async () => { 36 | expect.assertions(2) 37 | try { 38 | await start(shipit, 'rollback:init') 39 | } catch (err) { 40 | expect(shipit.remote).toBeCalledWith(readLinkCommand) 41 | expect(err.message).toBe('Remote servers are not synced.') 42 | } 43 | }) 44 | }) 45 | 46 | describe('bad release dirname', () => { 47 | beforeEach(() => { 48 | shipit.remote = jest.fn(async () => []) 49 | }) 50 | 51 | it('should return an error', async () => { 52 | expect.assertions(2) 53 | try { 54 | await start(shipit, 'rollback:init') 55 | } catch (err) { 56 | expect(shipit.remote).toBeCalledWith(readLinkCommand) 57 | expect(err.message).toBe('Cannot find current release dirname.') 58 | } 59 | }) 60 | }) 61 | }) 62 | 63 | describe('#getReleases', () => { 64 | describe('unsync server', () => { 65 | beforeEach(() => { 66 | shipit.remote = jest.fn(async command => { 67 | if (command === readLinkCommand) 68 | return [{ stdout: '/remote/deploy/releases/20141704123137' }] 69 | if (command === 'ls -r1 /remote/deploy/releases') 70 | return [ 71 | { stdout: '20141704123137\n20141704123134\n' }, 72 | { stdout: '20141704123137\n20141704123133\n' }, 73 | ] 74 | return null 75 | }) 76 | }) 77 | 78 | it('should return an error', async () => { 79 | expect.assertions(2) 80 | try { 81 | await start(shipit, 'rollback:init') 82 | } catch (err) { 83 | expect(shipit.remote).toBeCalledWith('ls -r1 /remote/deploy/releases') 84 | expect(err.message).toBe('Remote servers are not synced.') 85 | } 86 | }) 87 | }) 88 | 89 | describe('bad releases', () => { 90 | beforeEach(() => { 91 | shipit.remote = jest.fn(async command => { 92 | if (command === readLinkCommand) 93 | return [{ stdout: '/remote/deploy/releases/20141704123137' }] 94 | if (command === 'ls -r1 /remote/deploy/releases') return [] 95 | 96 | return null 97 | }) 98 | }) 99 | 100 | it('should return an error', async () => { 101 | expect.assertions(3) 102 | try { 103 | await start(shipit, 'rollback:init') 104 | } catch (err) { 105 | expect(shipit.remote).toBeCalledWith(readLinkCommand) 106 | expect(shipit.remote).toBeCalledWith('ls -r1 /remote/deploy/releases') 107 | expect(err.message).toBe('Cannot read releases.') 108 | } 109 | }) 110 | }) 111 | }) 112 | 113 | describe('release not exists', () => { 114 | beforeEach(() => { 115 | shipit.remote = jest.fn(async command => { 116 | if (command === readLinkCommand) 117 | return [{ stdout: '/remote/deploy/releases/20141704123137' }] 118 | if (command === 'ls -r1 /remote/deploy/releases') 119 | return [{ stdout: '20141704123137' }] 120 | 121 | return null 122 | }) 123 | }) 124 | 125 | it('should return an error', async () => { 126 | expect.assertions(3) 127 | try { 128 | await start(shipit, 'rollback:init') 129 | } catch (err) { 130 | expect(shipit.remote).toBeCalledWith(readLinkCommand) 131 | expect(shipit.remote).toBeCalledWith('ls -r1 /remote/deploy/releases') 132 | expect(err.message).toBe('Cannot rollback, release not found.') 133 | } 134 | }) 135 | }) 136 | 137 | describe('all good', () => { 138 | beforeEach(() => { 139 | shipit.remote = jest.fn(async command => { 140 | if (command === readLinkCommand) 141 | return [{ stdout: '/remote/deploy/releases/20141704123137\n' }] 142 | if (command === 'ls -r1 /remote/deploy/releases') 143 | return [{ stdout: '20141704123137\n20141704123136\n' }] 144 | return null 145 | }) 146 | }) 147 | 148 | it('define path', async () => { 149 | await start(shipit, 'rollback:init') 150 | expect(shipit.currentPath).toBe('/remote/deploy/current') 151 | expect(shipit.releasesPath).toBe('/remote/deploy/releases') 152 | expect(shipit.remote).toBeCalledWith(readLinkCommand) 153 | expect(shipit.remote).toBeCalledWith('ls -r1 /remote/deploy/releases') 154 | expect(shipit.releaseDirname).toBe('20141704123136') 155 | expect(shipit.releasePath).toBe('/remote/deploy/releases/20141704123136') 156 | }) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /packages/shipit-deploy/tests/integration.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import path from 'path' 3 | import { exec } from 'ssh-pool' 4 | 5 | const shipitCli = path.resolve(__dirname, '../../shipit-cli/src/cli.js') 6 | const shipitFile = path.resolve(__dirname, './sandbox/shipitfile.babel.js') 7 | const babelNode = require.resolve('@babel/node/bin/babel-node'); 8 | 9 | describe('shipit-cli', () => { 10 | it('should run a local task', async () => { 11 | try { 12 | await exec( 13 | `${babelNode} ${shipitCli} --shipitfile ${shipitFile} test deploy`, 14 | ) 15 | } catch (error) { 16 | // eslint-disable-next-line no-console 17 | console.error(error.stdout) 18 | 19 | throw error 20 | } 21 | 22 | const { stdout: lsReleases } = await exec( 23 | `${babelNode} ${shipitCli} --shipitfile ${shipitFile} test ls-releases`, 24 | ) 25 | 26 | const latestRelease = lsReleases 27 | .split('\n') 28 | .reverse()[2] 29 | .match(/\d{14}/)[0] 30 | 31 | const { stdout: lsCurrent } = await exec( 32 | `${babelNode} ${shipitCli} --shipitfile ${shipitFile} test ls-current`, 33 | ) 34 | 35 | const currentRelease = lsCurrent 36 | .split('\n')[3] 37 | .match(/releases\/(\d{14})/)[1] 38 | 39 | expect(latestRelease).toBe(currentRelease) 40 | }, 30000) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/shipit-deploy/tests/sandbox/shipitfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import shipitDeploy from '../..' 3 | 4 | export default shipit => { 5 | shipitDeploy(shipit) 6 | 7 | shipit.initConfig({ 8 | default: { 9 | key: './ssh/id_rsa', 10 | workspace: '/tmp/shipit-workspace', 11 | deployTo: '/tmp/shipit', 12 | repositoryUrl: 'https://github.com/shipitjs/shipit.git', 13 | ignores: ['.git', 'node_modules'], 14 | shallowClone: true, 15 | }, 16 | test: { 17 | servers: 'deploy@test.shipitjs.com', 18 | }, 19 | }) 20 | 21 | shipit.task('ls-releases', async () => { 22 | await shipit.remote('ls -lah /tmp/shipit/releases') 23 | }) 24 | 25 | shipit.task('ls-current', async () => { 26 | await shipit.remote('ls -lah /tmp/shipit/current') 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /packages/shipit-deploy/tests/util.js: -------------------------------------------------------------------------------- 1 | export function start(shipit, ...tasks) { 2 | return new Promise((resolve, reject) => { 3 | shipit.start(...tasks, err => { 4 | if (err) reject(err) 5 | else resolve() 6 | }) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /packages/ssh-pool/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/lib/**/*.js 3 | *.test.js 4 | -------------------------------------------------------------------------------- /packages/ssh-pool/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [5.3.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v5.2.0...v5.3.0) (2020-03-18) 7 | 8 | 9 | ### Features 10 | 11 | * add support of `asUser` ([#260](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/260)) ([4e79edb](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/4e79edb)) 12 | 13 | 14 | 15 | 16 | 17 | # [5.2.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v5.1.0...v5.2.0) (2020-03-07) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **windows:** cd must run the specified drive letter ([#252](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/252)) ([ab916a9](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/ab916a9)) 23 | * fix remote command wont reject on error, when cwd option is used ([#265](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/265)) ([986aec1](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/986aec1)) 24 | 25 | 26 | 27 | 28 | 29 | # [5.1.0](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v5.0.0...v5.1.0) (2019-08-28) 30 | 31 | 32 | ### Features 33 | 34 | * **ssh-pool:** Added ssh config array to remote server ([#248](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/248)) ([ba1d8c2](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/ba1d8c2)) 35 | 36 | 37 | 38 | 39 | 40 | ## [4.1.2](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/compare/v4.1.1...v4.1.2) (2018-11-04) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * **security:** use which instead of whereis ([#220](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/220)) ([6f46cad](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/6f46cad)) 46 | * use correct deprecation warning ([#219](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/issues/219)) ([e0c0fa5](https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy/commit/e0c0fa5)) 47 | 48 | 49 | 50 | 51 | 52 | 53 | # [4.1.0](https://github.com/babel/babel/tree/master/packages/babel-traverse/compare/v4.0.2...v4.1.0) (2018-04-27) 54 | 55 | 56 | ### Features 57 | 58 | * **ssh-pool:** add SSH Verbosity Levels ([#191](https://github.com/babel/babel/tree/master/packages/babel-traverse/issues/191)) ([327c63e](https://github.com/babel/babel/tree/master/packages/babel-traverse/commit/327c63e)) 59 | 60 | 61 | 62 | 63 | 64 | ## [4.0.2](https://github.com/babel/babel/tree/master/packages/babel-traverse/compare/v4.0.1...v4.0.2) (2018-03-25) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * be compatible with CommonJS ([abd2316](https://github.com/babel/babel/tree/master/packages/babel-traverse/commit/abd2316)) 70 | * fix scpCopyFromRemote & scpCopyToRemote ([01bc213](https://github.com/babel/babel/tree/master/packages/babel-traverse/commit/01bc213)), closes [#178](https://github.com/babel/babel/tree/master/packages/babel-traverse/issues/178) 71 | 72 | 73 | 74 | 75 | 76 | 77 | # 4.0.0 (2018-03-17) 78 | 79 | ## ssh-pool 80 | 81 | ### Features 82 | 83 | * Introduce a "tty" option in "run" method #56 84 | * Support "cwd" in "run" command #9 85 | * Expose a "isRsyncSupported" method 86 | 87 | ### Fixes 88 | 89 | * Fix parallel issues using scp copy shipitjs/ssh-pool#22 90 | * Fix command escaping #91 #152 91 | 92 | ### Docs 93 | 94 | * Update readme with new documentation 95 | 96 | ### Chores 97 | 98 | * Move to a Lerna repository 99 | * Add Codecov 100 | * Move to Jest for testing 101 | * Rewrite project in ES2017 targeting Node.js v6+ 102 | 103 | ### Deprecations 104 | 105 | * Deprecate automatic "sudo" removing when using "asUser" #56 #12 106 | * Deprecate "copy" method in favor of "copyToRemote", "copyFromRemote", "scpCopyToRemote" and "scpCopyFromRemote" 107 | * Deprecate using "deploy" as default user 108 | * Deprecate automatic "tty" when detecting "sudo" #56 109 | 110 | ### BREAKING CHANGES 111 | 112 | * Drop callbacks support and use native Promises 113 | * Standardise errors #154 114 | * Replace "cwd" behaviour in "run" command #9 115 | -------------------------------------------------------------------------------- /packages/ssh-pool/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Greg Bergé and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/ssh-pool/README.md: -------------------------------------------------------------------------------- 1 | # ssh-pool 2 | 3 | [![Build Status][build-badge]][build] 4 | [![version][version-badge]][package] 5 | [![MIT License][license-badge]][license] 6 | 7 | Run remote commands over a pool of server using SSH. 8 | 9 | ```sh 10 | npm install ssh-pool 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import { ConnectionPool } from 'ssh-pool' 17 | 18 | const pool = new ConnectionPool(['user@server1', 'user@server2']) 19 | 20 | async function run() { 21 | const results = await pool.run('hostname') 22 | console.log(results[0].stdout) // 'server1' 23 | console.log(results[1].stdout) // 'server2' 24 | } 25 | ``` 26 | 27 | ### new Connection(options) 28 | 29 | Create a new connection to run command on a remote server. 30 | 31 | **Parameters:** 32 | 33 | ``` 34 | @param {object} options Options 35 | @param {string|object} options.remote Remote 36 | @param {Stream} [options.stdout] Stdout stream 37 | @param {Stream} [options.stderr] Stderr stream 38 | @param {string} [options.key] SSH key 39 | @param {function} [options.log] Log method 40 | @param {boolean} [options.asUser] Use a custom user to run command 41 | @param {number} [options.verbosityLevel] SSH verbosity level: 0 (none), 1 (-v), 2 (-vv), 3+ (-vvv) 42 | ``` 43 | 44 | The remote can use the shorthand syntax or an object: 45 | 46 | ```js 47 | // You specify user and host 48 | new Connection({ remote: 'user@localhost' }) 49 | 50 | // You can specify a custom SSH port 51 | new Connection({ remote: 'user@localhost:4000' }) 52 | 53 | // You can also define remote using an object 54 | new Connection({ 55 | remote: { 56 | user: 'user', 57 | host: 'localhost', 58 | port: 4000, 59 | }, 60 | }) 61 | 62 | // When defined as an object you can add extra ssh parameters 63 | new Connection({ 64 | remote: { 65 | user: 'user', 66 | host: 'localhost', 67 | port: 4000, 68 | extraSshOptions: { 69 | ServerAliveInterval: '30', 70 | } 71 | }, 72 | }) 73 | ``` 74 | 75 | The log method is used to log output directly: 76 | 77 | ```js 78 | import { Connection } from 'ssh-pool' 79 | 80 | const connection = new Connection({ 81 | remote: 'localhost', 82 | log: (...args) => console.log(...args), 83 | }) 84 | 85 | connection.run('pwd') 86 | 87 | // Will output: 88 | // Running "pwd" on host "localhost". 89 | // @localhost /my/directory 90 | ``` 91 | 92 | ### connection.run(command, [options]) 93 | 94 | Run a command on the remote server, you can specify custom `childProcess.exec` options. 95 | 96 | **Parameters:** 97 | 98 | ``` 99 | @param {string} command Command to run 100 | @param {object} [options] Options 101 | @param {boolean} [options.tty] Force a TTY allocation. 102 | @returns {ExecResult} 103 | @throws {ExecError} 104 | ``` 105 | 106 | ```js 107 | // Run "ls" command on a remote server 108 | connection.run('ls').then(res => { 109 | console.log(res.stdout) // file1 file2 file3 110 | }) 111 | ``` 112 | 113 | ### connection.copyToRemote(src, dest, [options]) 114 | 115 | Copy a file or a directory from local to a remote server, you can specify custom `childProcess.exec` options. It uses rsync under the hood. 116 | 117 | **Parameters:** 118 | 119 | ``` 120 | * @param {string} src Source 121 | * @param {string} dest Destination 122 | * @param {object} [options] Options 123 | * @param {string[]} [options.ignores] Specify a list of files to ignore. 124 | * @param {string[]|string} [options.rsync] Specify a set of rsync arguments. 125 | * @returns {ExecResult} 126 | * @throws {ExecError} 127 | ``` 128 | 129 | ```js 130 | // Copy a local file to a remote file using Rsync 131 | connection.copyToRemote('./localfile', '/remote-file').then(() => { 132 | console.log('File copied!') 133 | }) 134 | ``` 135 | 136 | ### connection.copyFromRemote(src, dest, [options]) 137 | 138 | Copy a file or a directory from a remote server to local, you can specify custom `childProcess.exec` options. It uses rsync under the hood. 139 | 140 | **Parameters:** 141 | 142 | ``` 143 | * @param {string} src Source 144 | * @param {string} dest Destination 145 | * @param {object} [options] Options 146 | * @param {string[]} [options.ignores] Specify a list of files to ignore. 147 | * @param {string[]|string} [options.rsync] Specify a set of rsync arguments. 148 | * @returns {ExecResult} 149 | * @throws {ExecError} 150 | ``` 151 | 152 | ```js 153 | // Copy a remote file to a local file using Rsync 154 | connection.copyFromRemote('/remote-file', './local-file').then(() => { 155 | console.log('File copied!') 156 | }) 157 | ``` 158 | 159 | ### new ConnectionPool(connections, [options]) 160 | 161 | Create a new pool of connections and custom options for all connections. 162 | You can use either short syntax or connections to create a pool. 163 | 164 | ```js 165 | import { Connection, ConnectionPool } from 'ssh-pool' 166 | 167 | // Use shorthand. 168 | const pool = new ConnectionPool(['server1', 'server2']) 169 | 170 | // Use previously created connections. 171 | const connection1 = new Connection({ remote: 'server1' }) 172 | const connection2 = new Connection({ remote: 'server2' }) 173 | const pool = new ConnectionPool([connection1, connection2]) 174 | ``` 175 | 176 | Connection Pool accepts exactly the same methods as Connection. It runs commands in parallel on each server defined in the pool. You get an array of results. 177 | 178 | ### isRsyncSupported() 179 | 180 | Test if rsync is supported on the local machine. 181 | 182 | ```js 183 | import { isRsyncSupported } from 'ssh-pool' 184 | 185 | isRsyncSupported().then(supported => { 186 | if (supported) { 187 | console.log('Rsync is supported!') 188 | } else { 189 | console.log('Rsync is not supported!') 190 | } 191 | }) 192 | ``` 193 | 194 | ### exec(cmd, options, childModifier) 195 | 196 | Execute a command and return an object containing `{ child, stdout, stderr }`. 197 | 198 | ```js 199 | import { exec } from 'ssh-pool' 200 | 201 | exec('echo "hello"') 202 | .then(({ stdout }) => console.log(stdout)) 203 | .catch(({ stderr, stdout }) => console.error(stderr)) 204 | ``` 205 | 206 | ## License 207 | 208 | MIT 209 | 210 | [build-badge]: https://img.shields.io/travis/shipitjs/shipit.svg?style=flat-square 211 | [build]: https://travis-ci.org/shipitjs/shipit 212 | [version-badge]: https://img.shields.io/npm/v/ssh-pool.svg?style=flat-square 213 | [package]: https://www.npmjs.com/package/ssh-pool 214 | [license-badge]: https://img.shields.io/npm/l/ssh-pool.svg?style=flat-square 215 | [license]: https://github.com/shipitjs/shipit/blob/master/LICENSE 216 | -------------------------------------------------------------------------------- /packages/ssh-pool/__mocks__/child_process.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import EventEmitter from 'events' 3 | import { Readable } from 'stream' 4 | 5 | export const exec = jest.fn((command, options, cb) => { 6 | const child = new EventEmitter() 7 | child.stderr = new Readable() 8 | child.stderr._read = jest.fn() 9 | child.stdout = new Readable() 10 | child.stdout._read = jest.fn() 11 | 12 | process.nextTick(() => { 13 | cb(null, Buffer.from('stdout'), Buffer.from('stderr')) 14 | }) 15 | 16 | return child 17 | }) 18 | -------------------------------------------------------------------------------- /packages/ssh-pool/__mocks__/tmp.js: -------------------------------------------------------------------------------- 1 | export const tmpName = (options, cb) => cb(null, '/tmp/foo.tar.gz') 2 | -------------------------------------------------------------------------------- /packages/ssh-pool/__mocks__/which.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | let paths 3 | 4 | export const __setPaths__ = _paths => { 5 | paths = _paths 6 | } 7 | 8 | export default (name, cb) => { 9 | if (typeof paths[name] !== 'undefined') cb(null, paths[name]) 10 | else cb(new Error(`Could not find ${name} on your system`)) 11 | } 12 | -------------------------------------------------------------------------------- /packages/ssh-pool/examples/hostname.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const sshPool = require('../') 4 | 5 | const pool = new sshPool.ConnectionPool([ 6 | 'neoziro@localhost', 7 | 'neoziro@localhost', 8 | ]) 9 | 10 | pool.run('hostname').then(results => { 11 | console.log(results[0].stdout) 12 | console.log(results[1].stdout) 13 | }) 14 | -------------------------------------------------------------------------------- /packages/ssh-pool/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssh-pool", 3 | "version": "5.3.0", 4 | "description": "Run remote commands over a pool of server using SSH.", 5 | "engines": { 6 | "node": ">=6" 7 | }, 8 | "author": "Greg Bergé ", 9 | "license": "MIT", 10 | "repository": "https://github.com/shipitjs/shipit/tree/master/packages/shipit-deploy", 11 | "main": "lib/index.js", 12 | "keywords": [ 13 | "shipit", 14 | "automation", 15 | "deployment", 16 | "ssh" 17 | ], 18 | "scripts": { 19 | "prebuild": "rm -rf lib/", 20 | "build": "babel --config-file ../../babel.config.js -d lib --ignore \"**/*.test.js\" src", 21 | "prepublishOnly": "yarn run build" 22 | }, 23 | "dependencies": { 24 | "stream-line-wrapper": "^0.1.1", 25 | "tmp": "^0.1.0", 26 | "which": "^1.3.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/Connection.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import LineWrapper from 'stream-line-wrapper' 3 | import { tmpName as asyncTmpName } from 'tmp' 4 | import { formatRsyncCommand, isRsyncSupported } from './commands/rsync' 5 | import { formatSshCommand } from './commands/ssh' 6 | import { formatTarCommand } from './commands/tar' 7 | import { formatCdCommand } from './commands/cd' 8 | import { formatMkdirCommand } from './commands/mkdir' 9 | import { formatScpCommand } from './commands/scp' 10 | import { formatRawCommand } from './commands/raw' 11 | import { formatRmCommand } from './commands/rm' 12 | import { joinCommandArgs } from './commands/util' 13 | import { parseRemote, formatRemote } from './remote' 14 | import { exec, series, deprecateV3, deprecateV5 } from './util' 15 | 16 | const tmpName = async options => 17 | new Promise((resolve, reject) => 18 | asyncTmpName(options, (err, name) => { 19 | if (err) reject(err) 20 | else resolve(name) 21 | }), 22 | ) 23 | 24 | /** 25 | * An ExecResult returned when a command is executed with success. 26 | * @typedef {object} ExecResult 27 | * @property {Buffer} stdout 28 | * @property {Buffer} stderr 29 | * @property {ChildProcess} child 30 | */ 31 | 32 | /** 33 | * An ExecResult returned when a command is executed with success. 34 | * @typedef {object} MultipleExecResult 35 | * @property {Buffer} stdout 36 | * @property {Buffer} stderr 37 | * @property {ChildProcess[]} children 38 | */ 39 | 40 | /** 41 | * An ExecError returned when a command is executed with an error. 42 | * @typedef {Error} ExecError 43 | * @property {Buffer} stdout 44 | * @property {Buffer} stderr 45 | * @property {ChildProcess} child 46 | */ 47 | 48 | /** 49 | * Materialize a connection to a remote server. 50 | */ 51 | class Connection { 52 | /** 53 | * Initialize a new `Connection` with `options`. 54 | * 55 | * @param {object} options Options 56 | * @param {string|object} options.remote Remote 57 | * @param {Stream} [options.stdout] Stdout stream 58 | * @param {Stream} [options.stderr] Stderr stream 59 | * @param {string} [options.key] SSH key 60 | * @param {function} [options.log] Log method 61 | * @param {boolean} [options.asUser] Use a custom user to run command 62 | * @param {number} [options.verbosityLevel] The SSH verbosity level: 0 (none), 1 (-v), 2 (-vv), 3+ (-vvv) 63 | */ 64 | constructor(options = {}) { 65 | this.options = options 66 | this.remote = parseRemote(options.remote) 67 | this.remote.user = this.remote.user || 'deploy' 68 | } 69 | 70 | /** 71 | * Run a command remotely using SSH. 72 | * All exec options are also available. 73 | * 74 | * @see https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback 75 | * @param {string} command Command to run 76 | * @param {object} [options] Options 77 | * @param {boolean} [options.tty] Force a TTY allocation. 78 | * @returns {ExecResult} 79 | * @throws {ExecError} 80 | */ 81 | async run(command, { tty: ttyOption, cwd, ...cmdOptions } = {}) { 82 | let tty = ttyOption 83 | if (command.startsWith('sudo') && typeof ttyOption === 'undefined') { 84 | deprecateV3('You should set "tty" option explictly when you use "sudo".') 85 | tty = true 86 | } 87 | this.log('Running "%s" on host "%s".', command, this.remote.host) 88 | const cmd = this.buildSSHCommand(command, { tty, cwd }) 89 | return this.runLocally(cmd, cmdOptions) 90 | } 91 | 92 | /** 93 | * Run a copy command using either rsync or scp. 94 | * All exec options are also available. 95 | * 96 | * @see https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback 97 | * @deprecated 98 | * @param {string} src Source 99 | * @param {string} dest Destination 100 | * @param {object} [options] Options 101 | * @param {boolean} [options.direction] Specify "remoteToLocal" to copy from "remote". By default it will copy from remote. 102 | * @param {string[]} [options.ignores] Specify a list of files to ignore. 103 | * @param {string[]|string} [options.rsync] Specify a set of rsync arguments. 104 | * @returns {ExecResult|MultipleExecResult} 105 | * @throws {ExecError} 106 | */ 107 | async copy(src, dest, { direction, ...options } = {}) { 108 | deprecateV5( 109 | '"copy" method is deprecated, please use "copyToRemote", "copyFromRemote", "scpCopyToRemote" or "scpCopyFromRemote".', 110 | ) 111 | if (direction === 'remoteToLocal') 112 | return this.autoCopyFromRemote(src, dest, options) 113 | 114 | return this.autoCopyToRemote(src, dest, options) 115 | } 116 | 117 | /** 118 | * Run a copy from the local to the remote using rsync. 119 | * All exec options are also available. 120 | * 121 | * @see https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback 122 | * @param {string} src Source 123 | * @param {string} dest Destination 124 | * @param {object} [options] Options 125 | * @param {string[]} [options.ignores] Specify a list of files to ignore. 126 | * @param {string[]|string} [options.rsync] Specify a set of rsync arguments. 127 | * @returns {ExecResult} 128 | * @throws {ExecError} 129 | */ 130 | async copyToRemote(src, dest, options) { 131 | const remoteDest = `${formatRemote(this.remote)}:${dest}` 132 | return this.rsyncCopy(src, remoteDest, options) 133 | } 134 | 135 | /** 136 | * Run a copy from the remote to the local using rsync. 137 | * All exec options are also available. 138 | * 139 | * @see https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback 140 | * @param {string} src Source 141 | * @param {string} dest Destination 142 | * @param {object} [options] Options 143 | * @param {string[]} [options.ignores] Specify a list of files to ignore. 144 | * @param {string[]|string} [options.rsync] Specify a set of rsync arguments. 145 | * @returns {ExecResult} 146 | * @throws {ExecError} 147 | */ 148 | async copyFromRemote(src, dest, options) { 149 | const remoteSrc = `${formatRemote(this.remote)}:${src}` 150 | return this.rsyncCopy(remoteSrc, dest, options) 151 | } 152 | 153 | /** 154 | * Run a copy from the local to the remote using scp. 155 | * All exec options are also available. 156 | * 157 | * @see https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback 158 | * @param {string} src Source 159 | * @param {string} dest Destination 160 | * @param {object} [options] Options 161 | * @param {string[]} [options.ignores] Specify a list of files to ignore. 162 | * @param {...object} [cmdOptions] Command options 163 | * @returns {ExecResult} 164 | * @throws {ExecError} 165 | */ 166 | async scpCopyToRemote(src, dest, { ignores, ...cmdOptions } = {}) { 167 | const archive = path.basename(await tmpName({ postfix: '.tar.gz' })) 168 | const srcDir = path.dirname(src) 169 | const remoteDest = `${formatRemote(this.remote)}:${dest}` 170 | 171 | const compress = joinCommandArgs([ 172 | formatCdCommand({ folder: srcDir }), 173 | '&&', 174 | formatTarCommand({ 175 | mode: 'compress', 176 | file: path.basename(src), 177 | archive, 178 | excludes: ignores, 179 | }), 180 | ]) 181 | 182 | const createDestFolder = formatMkdirCommand({ folder: dest }) 183 | 184 | const copy = joinCommandArgs([ 185 | formatCdCommand({ folder: srcDir }), 186 | '&&', 187 | formatScpCommand({ 188 | port: this.remote.port, 189 | key: this.options.key, 190 | src: archive, 191 | dest: remoteDest, 192 | }), 193 | ]) 194 | 195 | const cleanSrc = joinCommandArgs([ 196 | formatCdCommand({ folder: srcDir }), 197 | '&&', 198 | formatRmCommand({ file: archive }), 199 | ]) 200 | 201 | const extract = joinCommandArgs([ 202 | formatCdCommand({ folder: dest }), 203 | '&&', 204 | formatTarCommand({ mode: 'extract', archive }), 205 | ]) 206 | 207 | const cleanDest = joinCommandArgs([ 208 | formatCdCommand({ folder: dest }), 209 | '&&', 210 | formatRmCommand({ file: archive }), 211 | ]) 212 | 213 | return this.aggregate([ 214 | () => this.runLocally(compress, cmdOptions), 215 | () => this.run(createDestFolder, cmdOptions), 216 | () => this.runLocally(copy, cmdOptions), 217 | () => this.runLocally(cleanSrc, cmdOptions), 218 | () => this.run(extract, cmdOptions), 219 | () => this.run(cleanDest, cmdOptions), 220 | ]) 221 | } 222 | 223 | /** 224 | * Run a copy from the remote to the local using scp. 225 | * All exec options are also available. 226 | * 227 | * @see https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback 228 | * @param {string} src Source 229 | * @param {string} dest Destination 230 | * @param {object} [options] Options 231 | * @param {string[]} [options.ignores] Specify a list of files to ignore. 232 | * @param {...object} [cmdOptions] Command options 233 | * @returns {MultipleExecResult} 234 | * @throws {ExecError} 235 | */ 236 | async scpCopyFromRemote(src, dest, { ignores, ...cmdOptions } = {}) { 237 | const archive = path.basename(await tmpName({ postfix: '.tar.gz' })) 238 | const srcDir = path.dirname(src) 239 | const srcArchive = path.join(srcDir, archive) 240 | const remoteSrcArchive = `${formatRemote(this.remote)}:${srcArchive}` 241 | 242 | const compress = joinCommandArgs([ 243 | formatCdCommand({ folder: srcDir }), 244 | '&&', 245 | formatTarCommand({ 246 | mode: 'compress', 247 | file: path.basename(src), 248 | archive, 249 | excludes: ignores, 250 | }), 251 | ]) 252 | 253 | const createDestFolder = formatMkdirCommand({ folder: dest }) 254 | 255 | const copy = formatScpCommand({ 256 | port: this.remote.port, 257 | key: this.options.key, 258 | src: remoteSrcArchive, 259 | dest, 260 | }) 261 | 262 | const cleanSrc = joinCommandArgs([ 263 | formatCdCommand({ folder: srcDir }), 264 | '&&', 265 | formatRmCommand({ file: archive }), 266 | ]) 267 | 268 | const extract = joinCommandArgs([ 269 | formatCdCommand({ folder: dest }), 270 | '&&', 271 | formatTarCommand({ mode: 'extract', archive }), 272 | ]) 273 | 274 | const cleanDest = joinCommandArgs([ 275 | formatCdCommand({ folder: dest }), 276 | '&&', 277 | formatRmCommand({ file: archive }), 278 | ]) 279 | 280 | return this.aggregate([ 281 | () => this.run(compress, cmdOptions), 282 | () => this.runLocally(createDestFolder, cmdOptions), 283 | () => this.runLocally(copy, cmdOptions), 284 | () => this.run(cleanSrc, cmdOptions), 285 | () => this.runLocally(extract, cmdOptions), 286 | () => this.runLocally(cleanDest, cmdOptions), 287 | ]) 288 | } 289 | 290 | /** 291 | * Build an SSH command. 292 | * 293 | * @private 294 | * @param {string} command 295 | * @param {object} options 296 | * @returns {string} 297 | */ 298 | buildSSHCommand(command, options) { 299 | return formatSshCommand({ 300 | port: this.remote.port, 301 | key: this.options.key, 302 | strict: this.options.strict, 303 | tty: this.options.tty, 304 | extraSshOptions: this.remote.extraSshOptions, 305 | verbosityLevel: this.options.verbosityLevel, 306 | remote: formatRemote(this.remote), 307 | command: formatRawCommand({ command, asUser: this.options.asUser }), 308 | ...options, 309 | }) 310 | } 311 | 312 | /** 313 | * Abstract method to copy using rsync. 314 | * 315 | * @private 316 | * @param {string} src 317 | * @param {string} dest 318 | * @param {object} options 319 | * @param {string[]|string} rsync Additional arguments 320 | * @param {string[]} ignores Files to ignore 321 | * @param {...object} cmdOptions Command options 322 | * @returns {ExecResult} 323 | * @throws {ExecError} 324 | */ 325 | async rsyncCopy(src, dest, { rsync, ignores, ...cmdOptions } = {}) { 326 | this.log('Copy "%s" to "%s" via rsync', src, dest) 327 | 328 | const sshCommand = formatSshCommand({ 329 | port: this.remote.port, 330 | key: this.options.key, 331 | strict: this.options.strict, 332 | extraSshOptions: this.remote.extraSshOptions, 333 | tty: this.options.tty, 334 | }) 335 | 336 | const cmd = formatRsyncCommand({ 337 | asUser: this.options.asUser, 338 | src, 339 | dest, 340 | remoteShell: sshCommand, 341 | additionalArgs: typeof rsync === 'string' ? [rsync] : rsync, 342 | excludes: ignores, 343 | }) 344 | 345 | return this.runLocally(cmd, cmdOptions) 346 | } 347 | 348 | /** 349 | * Automatic copy to remote method. 350 | * Choose rsync and fallback to scp if not available. 351 | * 352 | * @private 353 | * @param {string} src 354 | * @param {string} dest 355 | * @param {object} options 356 | * @returns {ExecResult|MultipleExecResult} 357 | * @throws {ExecError} 358 | */ 359 | async autoCopyToRemote(src, dest, options) { 360 | const rsyncAvailable = await isRsyncSupported() 361 | const method = rsyncAvailable ? 'copyToRemote' : 'scpCopyToRemote' 362 | return this[method](src, dest, options) 363 | } 364 | 365 | /** 366 | * Automatic copy from remote method. 367 | * Choose rsync and fallback to scp if not available. 368 | * 369 | * @private 370 | * @param {string} src 371 | * @param {string} dest 372 | * @param {object} options 373 | * @returns {ExecResult|MultipleExecResult} 374 | * @throws {ExecError} 375 | */ 376 | async autoCopyFromRemote(src, dest, options) { 377 | const rsyncAvailable = await isRsyncSupported() 378 | const method = rsyncAvailable ? 'copyFromRemote' : 'scpCopyFromRemote' 379 | return this[method](src, dest, options) 380 | } 381 | 382 | /** 383 | * Aggregate some exec tasks. 384 | * 385 | * @private 386 | * @param {Promise.[]} tasks An array of tasks 387 | * @returns {MultipleExecResult} 388 | * @throws {ExecError} 389 | */ 390 | async aggregate(tasks) { 391 | const results = await series(tasks) 392 | 393 | return results.reduce( 394 | (aggregate, result) => ({ 395 | stdout: String(aggregate.stdout) + String(result.stdout), 396 | stderr: String(aggregate.stderr) + String(result.stderr), 397 | children: [...aggregate.children, result.child], 398 | }), 399 | { 400 | stdout: '', 401 | stderr: '', 402 | children: [], 403 | }, 404 | ) 405 | } 406 | 407 | /** 408 | * Log using logger. 409 | * 410 | * @private 411 | * @param {...*} args 412 | */ 413 | log(...args) { 414 | if (this.options.log) this.options.log(...args) 415 | } 416 | 417 | /** 418 | * Method used to run a command locally. 419 | * 420 | * @private 421 | * @param {string} cmd 422 | * @param {object} [options] 423 | * @param {Buffer} [options.stdout] stdout buffer 424 | * @param {Buffer} [options.stderr] stderr buffer 425 | * @param {...object} [options.cmdOptions] Command options 426 | * @returns {ExecResult} 427 | * @throws {ExecError} 428 | */ 429 | async runLocally(cmd, { stdout, stderr, ...cmdOptions } = {}) { 430 | const stdoutPipe = stdout || this.options.stdout 431 | const stderrPipe = stderr || this.options.stderr 432 | 433 | return exec(cmd, cmdOptions, child => { 434 | if (stdoutPipe) 435 | child.stdout 436 | .pipe(new LineWrapper({ prefix: `@${this.remote.host} ` })) 437 | .pipe(stdoutPipe) 438 | 439 | if (stderrPipe) 440 | child.stderr 441 | .pipe(new LineWrapper({ prefix: `@${this.remote.host}-err ` })) 442 | .pipe(stderrPipe) 443 | }) 444 | } 445 | } 446 | 447 | export default Connection 448 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/Connection.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import stdMocks from 'std-mocks' 3 | import { exec } from 'child_process' 4 | import { __setPaths__ } from 'which' 5 | import * as util from './util' 6 | import Connection from './Connection' 7 | 8 | jest.mock('child_process') 9 | jest.mock('which') 10 | jest.mock('tmp') 11 | 12 | describe('Connection', () => { 13 | beforeEach(() => { 14 | exec.mockClear() 15 | util.deprecateV3 = jest.fn() 16 | __setPaths__({ rsync: '/bin/rsync' }) 17 | }) 18 | 19 | afterEach(() => { 20 | exec.mockClear() 21 | stdMocks.flush() 22 | stdMocks.restore() 23 | }) 24 | 25 | describe('constructor', () => { 26 | it('should accept remote object', () => { 27 | const connection = new Connection({ 28 | remote: { user: 'user', host: 'host' }, 29 | }) 30 | expect(connection.remote.user).toBe('user') 31 | expect(connection.remote.host).toBe('host') 32 | }) 33 | 34 | it('should accept remote string', () => { 35 | const connection = new Connection({ remote: 'user@host' }) 36 | expect(connection.remote.user).toBe('user') 37 | expect(connection.remote.host).toBe('host') 38 | }) 39 | }) 40 | 41 | describe('#run', () => { 42 | let connection 43 | 44 | beforeEach(() => { 45 | connection = new Connection({ remote: 'user@host' }) 46 | }) 47 | 48 | it('should call childProcess.exec', async () => { 49 | await connection.run('my-command -x', { cwd: '/root' }) 50 | 51 | expect(exec).toHaveBeenCalledWith( 52 | 'ssh user@host "cd /root > /dev/null && my-command -x; cd - > /dev/null"', 53 | { 54 | maxBuffer: 1024000, 55 | }, 56 | expect.any(Function), 57 | ) 58 | }) 59 | 60 | it('should escape double quotes', async () => { 61 | await connection.run('echo "ok"') 62 | 63 | expect(exec).toHaveBeenCalledWith( 64 | 'ssh user@host "echo \\"ok\\""', 65 | { 66 | maxBuffer: 1024000, 67 | }, 68 | expect.any(Function), 69 | ) 70 | }) 71 | 72 | it('should return result correctly', async () => { 73 | const result = await connection.run('my-command -x', { cwd: '/root' }) 74 | expect(result.stdout.toString()).toBe('stdout') 75 | expect(result.stderr.toString()).toBe('stderr') 76 | }) 77 | 78 | it('should handle tty', async () => { 79 | await connection.run('sudo my-command -x', { tty: true }) 80 | 81 | expect(exec).toHaveBeenCalledWith( 82 | 'ssh -tt user@host "sudo my-command -x"', 83 | { 84 | maxBuffer: 1024000, 85 | }, 86 | expect.any(Function), 87 | ) 88 | }) 89 | 90 | it('should copy args', async () => { 91 | await connection.run('my-command -x') 92 | await connection.run('my-command2 -x') 93 | 94 | expect(exec).toHaveBeenCalledWith( 95 | 'ssh user@host "my-command -x"', 96 | { maxBuffer: 1024000 }, 97 | expect.any(Function), 98 | ) 99 | expect(exec).toHaveBeenCalledWith( 100 | 'ssh user@host "my-command2 -x"', 101 | { maxBuffer: 1024000 }, 102 | expect.any(Function), 103 | ) 104 | }) 105 | 106 | it('should use key if present', async () => { 107 | connection = new Connection({ 108 | remote: 'user@host', 109 | key: '/path/to/key', 110 | }) 111 | 112 | await connection.run('my-command -x') 113 | expect(exec).toHaveBeenCalledWith( 114 | 'ssh -i /path/to/key user@host "my-command -x"', 115 | { maxBuffer: 1024000 }, 116 | expect.any(Function), 117 | ) 118 | }) 119 | 120 | it('should use port if present', async () => { 121 | connection = new Connection({ remote: 'user@host:12345' }) 122 | await connection.run('my-command -x') 123 | expect(exec).toHaveBeenCalledWith( 124 | 'ssh -p 12345 user@host "my-command -x"', 125 | { maxBuffer: 1024000 }, 126 | expect.any(Function), 127 | ) 128 | }) 129 | 130 | it('should use StrictHostKeyChecking if present', async () => { 131 | connection = new Connection({ 132 | remote: 'user@host', 133 | strict: 'no', 134 | }) 135 | await connection.run('my-command -x') 136 | expect(exec).toHaveBeenCalledWith( 137 | 'ssh -o StrictHostKeyChecking=no user@host "my-command -x"', 138 | { maxBuffer: 1024000 }, 139 | expect.any(Function), 140 | ) 141 | }) 142 | 143 | it('should use extra ssh options on remote if present', async () => { 144 | connection = new Connection({ 145 | remote: { 146 | host: 'host', 147 | user: 'user', 148 | extraSshOptions: { 149 | ExtraOption: 'option', 150 | SshForwardAgent: 'forward', 151 | } 152 | }, 153 | }) 154 | await connection.run('my-command -x') 155 | expect(exec).toHaveBeenCalledWith( 156 | 'ssh -o ExtraOption=option -o SshForwardAgent=forward user@host "my-command -x"', 157 | { maxBuffer: 1024000 }, 158 | expect.any(Function), 159 | ) 160 | }) 161 | 162 | it('should use port and key if both are present', async () => { 163 | connection = new Connection({ 164 | remote: 'user@host:12345', 165 | key: '/path/to/key', 166 | }) 167 | await connection.run('my-command -x') 168 | expect(exec).toHaveBeenCalledWith( 169 | 'ssh -p 12345 -i /path/to/key user@host "my-command -x"', 170 | { maxBuffer: 1024000 }, 171 | expect.any(Function), 172 | ) 173 | }) 174 | 175 | it('should log output', async () => { 176 | const log = jest.fn() 177 | connection = new Connection({ 178 | remote: 'user@host', 179 | log, 180 | stdout: process.stdout, 181 | stderr: process.stderr, 182 | }) 183 | 184 | stdMocks.use() 185 | const result = await connection.run('my-command -x') 186 | result.child.stdout.push('first line\n') 187 | result.child.stdout.push(null) 188 | 189 | result.child.stderr.push('an error\n') 190 | result.child.stderr.push(null) 191 | 192 | const output = stdMocks.flush() 193 | stdMocks.restore() 194 | expect(log).toHaveBeenCalledWith( 195 | 'Running "%s" on host "%s".', 196 | 'my-command -x', 197 | 'host', 198 | ) 199 | 200 | expect(output.stdout[0].toString()).toBe('@host first line\n') 201 | expect(output.stderr[0].toString()).toBe('@host-err an error\n') 202 | }) 203 | }) 204 | 205 | describe('#run asUser', () => { 206 | let connection 207 | 208 | beforeEach(() => { 209 | connection = new Connection({ 210 | remote: 'user@host', 211 | asUser: 'test', 212 | }) 213 | }) 214 | 215 | it('should handle sudo as user correctly', async () => { 216 | await connection.run('my-command -x', { tty: true }) 217 | 218 | expect(exec).toHaveBeenCalledWith( 219 | 'ssh -tt user@host "sudo -u test bash -c \'my-command -x\'"', 220 | { 221 | maxBuffer: 1000 * 1024, 222 | }, 223 | expect.any(Function), 224 | ) 225 | }) 226 | 227 | it('should handle sudo as user without double sudo', () => { 228 | connection.run('sudo my-command -x', { tty: true }) 229 | 230 | expect(exec).toHaveBeenCalledWith( 231 | 'ssh -tt user@host "sudo -u test bash -c \'my-command -x\'"', 232 | { 233 | maxBuffer: 1000 * 1024, 234 | }, 235 | expect.any(Function), 236 | ) 237 | }) 238 | }) 239 | 240 | describe('#copy', () => { 241 | let connection 242 | 243 | beforeEach(() => { 244 | connection = new Connection({ remote: 'user@host' }) 245 | }) 246 | 247 | it('should call cmd.spawn', async () => { 248 | await connection.copy('/src/dir', '/dest/dir') 249 | expect(exec).toHaveBeenCalledWith( 250 | 'rsync --archive --compress --rsh "ssh" /src/dir user@host:/dest/dir', 251 | { maxBuffer: 1024000 }, 252 | expect.any(Function), 253 | ) 254 | }) 255 | 256 | it('should accept "ignores" option', async () => { 257 | await connection.copy('/src/dir', '/dest/dir', { ignores: ['a', 'b'] }) 258 | expect(exec).toHaveBeenCalledWith( 259 | 'rsync --archive --compress --exclude "a" --exclude "b" --rsh "ssh" /src/dir user@host:/dest/dir', 260 | { maxBuffer: 1024000 }, 261 | expect.any(Function), 262 | ) 263 | }) 264 | 265 | it('should accept "direction" option', async () => { 266 | await connection.copy('/src/dir', '/dest/dir', { 267 | direction: 'remoteToLocal', 268 | }) 269 | expect(exec).toHaveBeenCalledWith( 270 | 'rsync --archive --compress --rsh "ssh" user@host:/src/dir /dest/dir', 271 | { maxBuffer: 1024000 }, 272 | expect.any(Function), 273 | ) 274 | }) 275 | 276 | it('should accept "rsync" option', async () => { 277 | await connection.copy('/src/dir', '/dest/dir', { 278 | rsync: '--info=progress2', 279 | }) 280 | expect(exec).toHaveBeenCalledWith( 281 | 'rsync --archive --compress --info=progress2 --rsh "ssh" /src/dir user@host:/dest/dir', 282 | { maxBuffer: 1024000 }, 283 | expect.any(Function), 284 | ) 285 | }) 286 | 287 | describe('without rsync available', () => { 288 | beforeEach(() => { 289 | __setPaths__({}) 290 | }) 291 | 292 | it('should use tar+scp', async () => { 293 | const result = await connection.copy('/a/b/c', '/x/y/z') 294 | expect(exec.mock.calls[0]).toEqual([ 295 | 'cd /a/b && tar -czf foo.tar.gz c', 296 | { maxBuffer: 1024000 }, 297 | expect.any(Function), 298 | ]) 299 | expect(exec.mock.calls[1]).toEqual([ 300 | 'ssh user@host "mkdir -p /x/y/z"', 301 | { maxBuffer: 1024000 }, 302 | expect.any(Function), 303 | ]) 304 | expect(exec.mock.calls[2]).toEqual([ 305 | 'cd /a/b && scp foo.tar.gz user@host:/x/y/z', 306 | { maxBuffer: 1024000 }, 307 | expect.any(Function), 308 | ]) 309 | expect(exec.mock.calls[3]).toEqual([ 310 | 'cd /a/b && rm foo.tar.gz', 311 | { maxBuffer: 1024000 }, 312 | expect.any(Function), 313 | ]) 314 | expect(exec.mock.calls[4]).toEqual([ 315 | 'ssh user@host "cd /x/y/z && tar -xzf foo.tar.gz"', 316 | { maxBuffer: 1024000 }, 317 | expect.any(Function), 318 | ]) 319 | expect(exec.mock.calls[5]).toEqual([ 320 | 'ssh user@host "cd /x/y/z && rm foo.tar.gz"', 321 | { maxBuffer: 1024000 }, 322 | expect.any(Function), 323 | ]) 324 | expect(result.stdout.toString()).toBe('stdout'.repeat(6)) 325 | expect(result.stderr.toString()).toBe('stderr'.repeat(6)) 326 | expect(result.children.length).toBe(6) 327 | }) 328 | 329 | it('should accept "direction" option when using tar+scp', async () => { 330 | const result = await connection.copy('/a/b/c', '/x/y/z', { 331 | direction: 'remoteToLocal', 332 | }) 333 | expect(exec.mock.calls[0]).toEqual([ 334 | 'ssh user@host "cd /a/b && tar -czf foo.tar.gz c"', 335 | { maxBuffer: 1024000 }, 336 | expect.any(Function), 337 | ]) 338 | expect(exec.mock.calls[1]).toEqual([ 339 | 'mkdir -p /x/y/z', 340 | { maxBuffer: 1024000 }, 341 | expect.any(Function), 342 | ]) 343 | expect(exec.mock.calls[2]).toEqual([ 344 | 'scp user@host:/a/b/foo.tar.gz /x/y/z', 345 | { maxBuffer: 1024000 }, 346 | expect.any(Function), 347 | ]) 348 | expect(exec.mock.calls[3]).toEqual([ 349 | 'ssh user@host "cd /a/b && rm foo.tar.gz"', 350 | { maxBuffer: 1024000 }, 351 | expect.any(Function), 352 | ]) 353 | expect(exec.mock.calls[4]).toEqual([ 354 | 'cd /x/y/z && tar -xzf foo.tar.gz', 355 | { maxBuffer: 1024000 }, 356 | expect.any(Function), 357 | ]) 358 | expect(exec.mock.calls[5]).toEqual([ 359 | 'cd /x/y/z && rm foo.tar.gz', 360 | { maxBuffer: 1024000 }, 361 | expect.any(Function), 362 | ]) 363 | expect(result.stdout.toString()).toBe('stdout'.repeat(6)) 364 | expect(result.stderr.toString()).toBe('stderr'.repeat(6)) 365 | expect(result.children.length).toBe(6) 366 | }) 367 | 368 | it('should accept port and key', async () => { 369 | connection = new Connection({ 370 | remote: 'user@host:12345', 371 | key: '/path/to/key', 372 | }) 373 | const result = await connection.copy('/a/b/c', '/x/y/z') 374 | expect(exec.mock.calls[0]).toEqual([ 375 | 'cd /a/b && tar -czf foo.tar.gz c', 376 | { maxBuffer: 1024000 }, 377 | expect.any(Function), 378 | ]) 379 | expect(exec.mock.calls[1]).toEqual([ 380 | 'ssh -p 12345 -i /path/to/key user@host "mkdir -p /x/y/z"', 381 | { maxBuffer: 1024000 }, 382 | expect.any(Function), 383 | ]) 384 | expect(exec.mock.calls[2]).toEqual([ 385 | 'cd /a/b && scp -P 12345 -i /path/to/key foo.tar.gz user@host:/x/y/z', 386 | { maxBuffer: 1024000 }, 387 | expect.any(Function), 388 | ]) 389 | expect(exec.mock.calls[3]).toEqual([ 390 | 'cd /a/b && rm foo.tar.gz', 391 | { maxBuffer: 1024000 }, 392 | expect.any(Function), 393 | ]) 394 | expect(exec.mock.calls[4]).toEqual([ 395 | 'ssh -p 12345 -i /path/to/key user@host "cd /x/y/z && tar -xzf foo.tar.gz"', 396 | { maxBuffer: 1024000 }, 397 | expect.any(Function), 398 | ]) 399 | expect(exec.mock.calls[5]).toEqual([ 400 | 'ssh -p 12345 -i /path/to/key user@host "cd /x/y/z && rm foo.tar.gz"', 401 | { maxBuffer: 1024000 }, 402 | expect.any(Function), 403 | ]) 404 | expect(result.stdout.toString()).toBe('stdout'.repeat(6)) 405 | expect(result.stderr.toString()).toBe('stderr'.repeat(6)) 406 | expect(result.children.length).toBe(6) 407 | }) 408 | }) 409 | 410 | it('should use key if present', async () => { 411 | connection = new Connection({ 412 | remote: 'user@host', 413 | key: '/path/to/key', 414 | }) 415 | await connection.copy('/src/dir', '/dest/dir') 416 | expect(exec).toHaveBeenCalledWith( 417 | 'rsync --archive --compress --rsh "ssh -i /path/to/key" /src/dir user@host:/dest/dir', 418 | { maxBuffer: 1024000 }, 419 | expect.any(Function), 420 | ) 421 | }) 422 | 423 | it('should use port if present', async () => { 424 | connection = new Connection({ 425 | remote: 'user@host:12345', 426 | }) 427 | await connection.copy('/src/dir', '/dest/dir') 428 | expect(exec).toHaveBeenCalledWith( 429 | 'rsync --archive --compress --rsh "ssh -p 12345" /src/dir user@host:/dest/dir', 430 | { maxBuffer: 1024000 }, 431 | expect.any(Function), 432 | ) 433 | }) 434 | 435 | it('should use StrictHostKeyChecking if present', async () => { 436 | connection = new Connection({ 437 | remote: 'user@host', 438 | strict: 'yes', 439 | }) 440 | await connection.copy('/src/dir', '/dest/dir') 441 | expect(exec).toHaveBeenCalledWith( 442 | 'rsync --archive --compress --rsh "ssh -o StrictHostKeyChecking=yes" /src/dir user@host:/dest/dir', 443 | { maxBuffer: 1024000 }, 444 | expect.any(Function), 445 | ) 446 | }) 447 | 448 | it('should use port and key if both are present', async () => { 449 | connection = new Connection({ 450 | remote: 'user@host:12345', 451 | key: '/path/to/key', 452 | }) 453 | await connection.copy('/src/dir', '/dest/dir') 454 | expect(exec).toHaveBeenCalledWith( 455 | 'rsync --archive --compress --rsh "ssh -p 12345 -i /path/to/key" /src/dir user@host:/dest/dir', 456 | { maxBuffer: 1024000 }, 457 | expect.any(Function), 458 | ) 459 | }) 460 | 461 | it('should log output', async () => { 462 | const log = jest.fn() 463 | connection = new Connection({ 464 | remote: 'user@host', 465 | log, 466 | stdout: process.stdout, 467 | stderr: process.stderr, 468 | }) 469 | 470 | stdMocks.use() 471 | const result = await connection.copy('/src/dir', '/dest/dir') 472 | result.child.stdout.push('first line\n') 473 | result.child.stdout.push(null) 474 | 475 | result.child.stderr.push('an error\n') 476 | result.child.stderr.push(null) 477 | 478 | const output = stdMocks.flush() 479 | expect(log).toHaveBeenCalledWith( 480 | 'Copy "%s" to "%s" via rsync', 481 | '/src/dir', 482 | 'user@host:/dest/dir', 483 | ) 484 | expect(output.stdout.toString()).toContain('@host first line\n') 485 | expect(output.stderr.toString()).toContain('@host-err an error\n') 486 | }) 487 | }) 488 | }) 489 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/ConnectionPool.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import Connection from './Connection' 3 | 4 | class ConnectionPool { 5 | /** 6 | * Initialize a new `ConnectionPool` with `connections`. 7 | * All Connection options are also supported. 8 | * 9 | * @param {Connection|string[]} connections Connections 10 | * @param {object} [options] Options 11 | */ 12 | constructor(connections, options) { 13 | this.connections = connections.map(connection => { 14 | if (connection instanceof Connection) return connection 15 | return new Connection({ remote: connection, ...options }) 16 | }) 17 | } 18 | } 19 | 20 | ;[ 21 | 'run', 22 | 'copy', 23 | 'copyToRemote', 24 | 'copyFromRemote', 25 | 'scpCopyToRemote', 26 | 'scpCopyFromRemote', 27 | ].forEach(method => { 28 | ConnectionPool.prototype[method] = function(...args) { 29 | return Promise.all( 30 | this.connections.map(connection => connection[method](...args)), 31 | ) 32 | } 33 | }) 34 | 35 | export default ConnectionPool 36 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/ConnectionPool.test.js: -------------------------------------------------------------------------------- 1 | import { __setPaths__ } from 'which' 2 | import { exec } from 'child_process' 3 | import * as util from './util' 4 | import Connection from './Connection' 5 | import ConnectionPool from './ConnectionPool' 6 | 7 | jest.mock('which') 8 | jest.mock('child_process') 9 | 10 | describe('ConnectionPool', () => { 11 | beforeEach(() => { 12 | util.deprecateV3 = jest.fn() 13 | __setPaths__({ rsync: '/bin/rsync' }) 14 | }) 15 | 16 | describe('constructor', () => { 17 | it('should be possible to create a new ConnectionPool using shorthand syntax', () => { 18 | const pool = new ConnectionPool(['myserver', 'myserver2']) 19 | expect(pool.connections[0].remote).toEqual({ 20 | user: 'deploy', 21 | host: 'myserver', 22 | }) 23 | 24 | expect(pool.connections[1].remote).toEqual({ 25 | user: 'deploy', 26 | host: 'myserver2', 27 | }) 28 | }) 29 | 30 | it('should be possible to create a new ConnectionPool with long syntax', () => { 31 | const connection1 = new Connection({ remote: 'myserver' }) 32 | const connection2 = new Connection({ remote: 'myserver2' }) 33 | const pool = new ConnectionPool([connection1, connection2]) 34 | expect(pool.connections[0]).toBe(connection1) 35 | expect(pool.connections[1]).toBe(connection2) 36 | }) 37 | }) 38 | 39 | describe('#run', () => { 40 | let connection1 41 | let connection2 42 | let pool 43 | 44 | beforeEach(() => { 45 | connection1 = new Connection({ remote: 'myserver' }) 46 | connection2 = new Connection({ remote: 'myserver2' }) 47 | pool = new ConnectionPool([connection1, connection2]) 48 | }) 49 | 50 | it('should run command on each connection', async () => { 51 | const results = await pool.run('my-command -x', { cwd: '/root' }) 52 | expect(results[0].stdout.toString()).toBe('stdout') 53 | expect(results[1].stdout.toString()).toBe('stdout') 54 | expect(exec).toHaveBeenCalledWith( 55 | 'ssh deploy@myserver2 "cd /root > /dev/null && my-command -x; cd - > /dev/null"', 56 | { 57 | maxBuffer: 1000 * 1024, 58 | }, 59 | expect.any(Function), 60 | ) 61 | expect(exec).toHaveBeenCalledWith( 62 | 'ssh deploy@myserver "cd /root > /dev/null && my-command -x; cd - > /dev/null"', 63 | { 64 | maxBuffer: 1000 * 1024, 65 | }, 66 | expect.any(Function), 67 | ) 68 | }) 69 | }) 70 | 71 | describe('#copy', () => { 72 | let connection1 73 | let connection2 74 | let pool 75 | 76 | beforeEach(() => { 77 | connection1 = new Connection({ remote: 'myserver' }) 78 | connection2 = new Connection({ remote: 'myserver2' }) 79 | pool = new ConnectionPool([connection1, connection2]) 80 | }) 81 | 82 | it('should run command on each connection', async () => { 83 | const results = await pool.copy('/src/dir', '/dest/dir') 84 | expect(results[0].stdout.toString()).toBe('stdout') 85 | expect(results[1].stdout.toString()).toBe('stdout') 86 | 87 | expect(exec).toHaveBeenCalledWith( 88 | 'rsync --archive --compress --rsh "ssh" /src/dir deploy@myserver:/dest/dir', 89 | { maxBuffer: 1024000 }, 90 | expect.any(Function), 91 | ) 92 | 93 | expect(exec).toHaveBeenCalledWith( 94 | 'rsync --archive --compress --rsh "ssh" /src/dir deploy@myserver2:/dest/dir', 95 | { maxBuffer: 1024000 }, 96 | expect.any(Function), 97 | ) 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/cd.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { joinCommandArgs, requireArgs } from './util' 3 | 4 | const isWin = /^win/.test(process.platform) 5 | 6 | export function formatCdCommand({ folder }) { 7 | requireArgs(['folder'], { folder }, 'cd') 8 | const args = ['cd', folder] 9 | const { root } = path.parse(folder) 10 | const drive = root.replace(path.sep, '') 11 | if (isWin && root !== '/') { 12 | args.push(`&& ${drive}`) 13 | } 14 | return joinCommandArgs(args) 15 | } 16 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/cd.test.js: -------------------------------------------------------------------------------- 1 | import { formatCdCommand } from './cd' 2 | 3 | describe('mkdir', () => { 4 | describe('#formatCdCommand', () => { 5 | describe('without "folder"', () => { 6 | it('should throw an error', () => { 7 | expect(() => formatCdCommand({})).toThrow( 8 | '"folder" argument is required in "cd" command', 9 | ) 10 | }) 11 | }) 12 | 13 | it('should format command', () => { 14 | expect(formatCdCommand({ folder: 'xxx' })).toBe('cd xxx') 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/mkdir.js: -------------------------------------------------------------------------------- 1 | import { joinCommandArgs, requireArgs } from './util' 2 | import { formatRawCommand } from './raw' 3 | 4 | export function formatMkdirCommand({ asUser, folder }) { 5 | requireArgs(['folder'], { folder }, 'mkdir') 6 | return formatRawCommand({ 7 | asUser, 8 | command: joinCommandArgs(['mkdir', '-p', folder]), 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/mkdir.test.js: -------------------------------------------------------------------------------- 1 | import { formatMkdirCommand } from './mkdir' 2 | 3 | describe('mkdir', () => { 4 | describe('#formatMkdirCommand', () => { 5 | describe('without "folder"', () => { 6 | it('should throw an error', () => { 7 | expect(() => formatMkdirCommand({})).toThrow( 8 | '"folder" argument is required in "mkdir" command', 9 | ) 10 | }) 11 | }) 12 | 13 | it('should format command', () => { 14 | expect(formatMkdirCommand({ folder: 'xxx' })).toBe('mkdir -p xxx') 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/raw.js: -------------------------------------------------------------------------------- 1 | import { joinCommandArgs } from './util' 2 | import { deprecateV3 } from '../util' 3 | 4 | const SUDO_REGEXP = /sudo\s/ 5 | 6 | export function formatRawCommand({ asUser, command }) { 7 | let args = [] 8 | if (asUser) args = [...args, 'sudo', '-u', asUser] 9 | // Deprecate 10 | if (asUser && command) { 11 | if (command.match(SUDO_REGEXP)) { 12 | deprecateV3( 13 | 'You should not use "sudo" and "asUser" options together. Please remove "sudo" from command.', 14 | ) 15 | } 16 | 17 | const commandWithoutSudo = command.replace(SUDO_REGEXP, ''); 18 | args = [...args, 'bash', '-c', `'${commandWithoutSudo}'`] 19 | } else if (command) args = [...args, command] 20 | return joinCommandArgs(args) 21 | } 22 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/raw.test.js: -------------------------------------------------------------------------------- 1 | import * as util from '../util' 2 | import { formatRawCommand } from './raw' 3 | 4 | describe('raw', () => { 5 | beforeEach(() => { 6 | util.deprecateV3 = jest.fn() 7 | }) 8 | 9 | describe('#formatRawCommand', () => { 10 | it('should support command', () => { 11 | expect(formatRawCommand({ command: 'echo "ok"' })).toBe('echo "ok"') 12 | }) 13 | 14 | it('should support asUser', () => { 15 | expect(formatRawCommand({ asUser: 'foo', command: 'echo "ok"' })).toBe( 16 | 'sudo -u foo bash -c \'echo "ok"\'', 17 | ) 18 | 19 | expect( 20 | formatRawCommand({ asUser: 'foo', command: 'sudo echo "ok"' }), 21 | ).toBe('sudo -u foo bash -c \'echo "ok"\'') 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/rm.js: -------------------------------------------------------------------------------- 1 | import { joinCommandArgs, requireArgs } from './util' 2 | import { formatRawCommand } from './raw' 3 | 4 | export function formatRmCommand({ asUser, file }) { 5 | requireArgs(['file'], { file }, 'rm') 6 | return formatRawCommand({ 7 | asUser, 8 | command: joinCommandArgs(['rm', file]), 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/rm.test.js: -------------------------------------------------------------------------------- 1 | import { formatRmCommand } from './rm' 2 | 3 | describe('rm', () => { 4 | describe('#formatRmCommand', () => { 5 | describe('without "file"', () => { 6 | it('should throw an error', () => { 7 | expect(() => formatRmCommand({})).toThrow( 8 | '"file" argument is required in "rm" command', 9 | ) 10 | }) 11 | }) 12 | 13 | it('should format command', () => { 14 | expect(formatRmCommand({ file: 'xxx' })).toBe('rm xxx') 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/rsync.js: -------------------------------------------------------------------------------- 1 | import which from 'which' 2 | import { wrapCommand, joinCommandArgs, requireArgs } from './util' 3 | 4 | export async function isRsyncSupported() { 5 | return new Promise(resolve => which('rsync', err => resolve(!err))) 6 | } 7 | 8 | function formatExcludes(excludes) { 9 | return excludes.reduce( 10 | (args, current) => [...args, '--exclude', `"${current}"`], 11 | [], 12 | ) 13 | } 14 | 15 | export function formatRsyncCommand({ 16 | asUser, 17 | src, 18 | dest, 19 | excludes, 20 | additionalArgs, 21 | remoteShell, 22 | }) { 23 | requireArgs(['src', 'dest'], { src, dest }, 'rsync') 24 | let args = ['rsync', '--archive', '--compress'] 25 | if (asUser) args = [...args, '--rsync-path', wrapCommand(`sudo -u ${asUser} rsync`)] 26 | if (additionalArgs) args = [...args, ...additionalArgs] 27 | if (excludes) args = [...args, ...formatExcludes(excludes)] 28 | if (remoteShell) args = [...args, '--rsh', wrapCommand(remoteShell)] 29 | args = [...args, src, dest] 30 | return joinCommandArgs(args) 31 | } 32 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/rsync.test.js: -------------------------------------------------------------------------------- 1 | import { formatRsyncCommand } from './rsync' 2 | 3 | describe('rsync', () => { 4 | describe('#formatRsyncCommand', () => { 5 | describe('without "src" or "dest"', () => { 6 | it('should throw an error', () => { 7 | expect(() => formatRsyncCommand({})).toThrow( 8 | '"src" argument is required in "rsync" command', 9 | ) 10 | expect(() => formatRsyncCommand({ dest: 'foo' })).toThrow( 11 | '"src" argument is required in "rsync" command', 12 | ) 13 | expect(() => formatRsyncCommand({ src: 'foo' })).toThrow( 14 | '"dest" argument is required in "rsync" command', 15 | ) 16 | }) 17 | }) 18 | 19 | it('should support src and dest', () => { 20 | expect(formatRsyncCommand({ src: 'file.js', dest: 'foo/' })).toBe( 21 | 'rsync --archive --compress file.js foo/', 22 | ) 23 | }) 24 | 25 | it('should support additionalArgs', () => { 26 | expect( 27 | formatRsyncCommand({ 28 | src: 'file.js', 29 | dest: 'foo/', 30 | additionalArgs: ['--max-size=10'], 31 | }), 32 | ).toBe('rsync --archive --compress --max-size=10 file.js foo/') 33 | }) 34 | 35 | it('should support excludes', () => { 36 | expect( 37 | formatRsyncCommand({ 38 | src: 'file.js', 39 | dest: 'foo/', 40 | excludes: ['foo'], 41 | }), 42 | ).toBe('rsync --archive --compress --exclude "foo" file.js foo/') 43 | }) 44 | 45 | it('should support remoteShell', () => { 46 | expect( 47 | formatRsyncCommand({ 48 | src: 'file.js', 49 | dest: 'foo/', 50 | remoteShell: 'ssh', 51 | }), 52 | ).toBe('rsync --archive --compress --rsh "ssh" file.js foo/') 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/scp.js: -------------------------------------------------------------------------------- 1 | import { joinCommandArgs, requireArgs } from './util' 2 | 3 | export function formatScpCommand({ port, key, src, dest }) { 4 | requireArgs(['src', 'dest'], { src, dest }, 'scp') 5 | let args = ['scp'] 6 | if (port) args = [...args, '-P', port] 7 | if (key) args = [...args, '-i', key] 8 | args = [...args, src, dest] 9 | return joinCommandArgs(args) 10 | } 11 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/scp.test.js: -------------------------------------------------------------------------------- 1 | import { formatScpCommand } from './scp' 2 | 3 | describe('scp', () => { 4 | describe('#formatScpCommand', () => { 5 | describe('without "src" or "dest"', () => { 6 | it('should throw an error', () => { 7 | expect(() => formatScpCommand({})).toThrow( 8 | '"src" argument is required in "scp" command', 9 | ) 10 | expect(() => formatScpCommand({ dest: 'foo' })).toThrow( 11 | '"src" argument is required in "scp" command', 12 | ) 13 | expect(() => formatScpCommand({ src: 'foo' })).toThrow( 14 | '"dest" argument is required in "scp" command', 15 | ) 16 | }) 17 | }) 18 | 19 | it('should support src and dest', () => { 20 | expect(formatScpCommand({ src: 'file.js', dest: 'foo/' })).toBe( 21 | 'scp file.js foo/', 22 | ) 23 | }) 24 | 25 | it('should support port', () => { 26 | expect( 27 | formatScpCommand({ src: 'file.js', dest: 'foo/', port: 3000 }), 28 | ).toBe('scp -P 3000 file.js foo/') 29 | }) 30 | 31 | it('should support key', () => { 32 | expect( 33 | formatScpCommand({ 34 | src: 'file.js', 35 | dest: 'foo/', 36 | key: 'foo', 37 | }), 38 | ).toBe('scp -i foo file.js foo/') 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/ssh.js: -------------------------------------------------------------------------------- 1 | import { joinCommandArgs, wrapCommand } from './util' 2 | 3 | function wrapCwd(cwd, command) { 4 | return `cd ${cwd} > /dev/null && ${command}; cd - > /dev/null` 5 | } 6 | 7 | export function formatSshCommand({ 8 | port, 9 | key, 10 | strict, 11 | tty, 12 | remote, 13 | cwd, 14 | command, 15 | extraSshOptions, 16 | verbosityLevel, 17 | }) { 18 | let args = ['ssh'] 19 | if (verbosityLevel) { 20 | switch (verbosityLevel) { 21 | case verbosityLevel <= 0: 22 | break 23 | case 1: 24 | args = [...args, '-v'] 25 | break 26 | case 2: 27 | args = [...args, '-vv'] 28 | break 29 | default: 30 | args = [...args, '-vvv'] 31 | break 32 | } 33 | } 34 | if (tty) args = [...args, '-tt'] 35 | if (port) args = [...args, '-p', port] 36 | if (key) args = [...args, '-i', key] 37 | if(extraSshOptions && typeof extraSshOptions === 'object') { 38 | Object.keys(extraSshOptions).forEach((sshOptionsKey) => { 39 | args = [...args, '-o', `${sshOptionsKey}=${extraSshOptions[sshOptionsKey]}`] 40 | }) 41 | } 42 | if (strict !== undefined) 43 | args = [...args, '-o', `StrictHostKeyChecking=${strict}`] 44 | if (remote) args = [...args, remote] 45 | 46 | const cwdCommand = cwd ? wrapCwd(cwd, command) : command 47 | if (command) args = [...args, wrapCommand(cwdCommand)] 48 | return joinCommandArgs(args) 49 | } 50 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/ssh.test.js: -------------------------------------------------------------------------------- 1 | import { formatSshCommand } from './ssh' 2 | 3 | describe('ssh', () => { 4 | describe('#formatSshCommand', () => { 5 | it('should support tty', () => { 6 | expect(formatSshCommand({ tty: true })).toBe('ssh -tt') 7 | }) 8 | 9 | it('should support port', () => { 10 | expect(formatSshCommand({ port: 3000 })).toBe('ssh -p 3000') 11 | }) 12 | 13 | it('should support key', () => { 14 | expect(formatSshCommand({ key: 'foo' })).toBe('ssh -i foo') 15 | }) 16 | 17 | it('should support strict', () => { 18 | expect(formatSshCommand({ strict: true })).toBe( 19 | 'ssh -o StrictHostKeyChecking=true', 20 | ) 21 | expect(formatSshCommand({ strict: false })).toBe( 22 | 'ssh -o StrictHostKeyChecking=false', 23 | ) 24 | expect(formatSshCommand({ strict: 'no' })).toBe( 25 | 'ssh -o StrictHostKeyChecking=no', 26 | ) 27 | expect(formatSshCommand({ strict: 'yes' })).toBe( 28 | 'ssh -o StrictHostKeyChecking=yes', 29 | ) 30 | }) 31 | 32 | it('should support extra ssh options', () => { 33 | expect(formatSshCommand({ extraSshOptions: {ExtraOption: 'test option', MoreOptions: 'more option'} })).toBe( 34 | 'ssh -o ExtraOption=test option -o MoreOptions=more option', 35 | ) 36 | }) 37 | 38 | it('should support remote', () => { 39 | expect( 40 | formatSshCommand({ 41 | remote: 'user@host', 42 | }), 43 | ).toBe('ssh user@host') 44 | }) 45 | 46 | it('should support command', () => { 47 | expect( 48 | formatSshCommand({ 49 | remote: 'user@host', 50 | command: 'echo "ok"', 51 | }), 52 | ).toBe('ssh user@host "echo \\"ok\\""') 53 | }) 54 | 55 | it('should support cwd', () => { 56 | expect( 57 | formatSshCommand({ 58 | remote: 'user@host', 59 | command: 'echo "ok"', 60 | cwd: '/usr', 61 | }), 62 | ).toBe( 63 | 'ssh user@host "cd /usr > /dev/null && echo \\"ok\\"; cd - > /dev/null"', 64 | ) 65 | }) 66 | 67 | it('should support verbosityLevel', () => { 68 | expect( 69 | formatSshCommand({ 70 | remote: 'user@host', 71 | command: 'echo "ok"', 72 | cwd: '/usr', 73 | verbosityLevel: 2, 74 | }), 75 | ).toBe( 76 | 'ssh -vv user@host "cd /usr > /dev/null && echo \\"ok\\"; cd - > /dev/null"', 77 | ) 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/tar.js: -------------------------------------------------------------------------------- 1 | import { joinCommandArgs, requireArgs } from './util' 2 | 3 | function formatExcludes(excludes) { 4 | return excludes.reduce( 5 | (args, current) => [...args, '--exclude', `"${current}"`], 6 | [], 7 | ) 8 | } 9 | 10 | export function formatTarCommand({ file, archive, excludes, mode }) { 11 | let args = ['tar'] 12 | switch (mode) { 13 | case 'compress': { 14 | requireArgs(['file', 'archive'], { file, archive }, 'tar') 15 | if (excludes) args = [...args, ...formatExcludes(excludes)] 16 | args = [...args, '-czf', archive, file] 17 | return joinCommandArgs(args) 18 | } 19 | case 'extract': { 20 | requireArgs(['archive'], { file, archive }, 'tar') 21 | args = [...args, '-xzf', archive] 22 | return joinCommandArgs(args) 23 | } 24 | default: 25 | throw new Error( 26 | `mode "${mode}" is not valid in "tar" command (valid values: ["extract", "compress"])`, 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/tar.test.js: -------------------------------------------------------------------------------- 1 | import { formatTarCommand } from './tar' 2 | 3 | describe('tar', () => { 4 | describe('#formatTarCommand', () => { 5 | describe('mode: "compress", without "file" or "archive"', () => { 6 | it('should throw an error', () => { 7 | expect(() => formatTarCommand({ mode: 'compress' })).toThrow( 8 | '"file" argument is required in "tar" command', 9 | ) 10 | expect(() => 11 | formatTarCommand({ mode: 'compress', archive: 'foo' }), 12 | ).toThrow('"file" argument is required in "tar" command') 13 | expect(() => 14 | formatTarCommand({ mode: 'compress', file: 'foo' }), 15 | ).toThrow('"archive" argument is required in "tar" command') 16 | }) 17 | }) 18 | 19 | describe('mode: "extract", without "archive"', () => { 20 | it('should throw an error', () => { 21 | expect(() => formatTarCommand({ mode: 'extract' })).toThrow( 22 | '"archive" argument is required in "tar" command', 23 | ) 24 | }) 25 | }) 26 | 27 | describe('without a valid "mode"', () => { 28 | it('should throw an error', () => { 29 | expect(() => formatTarCommand({ file: 'foo', archive: 'foo' })).toThrow( 30 | 'mode "undefined" is not valid in "tar" command (valid values: ["extract", "compress"])', 31 | ) 32 | expect(() => 33 | formatTarCommand({ file: 'foo', archive: 'foo', mode: 'foo' }), 34 | ).toThrow( 35 | 'mode "foo" is not valid in "tar" command (valid values: ["extract", "compress"])', 36 | ) 37 | }) 38 | }) 39 | 40 | it('should support compress mode', () => { 41 | expect( 42 | formatTarCommand({ 43 | file: 'file', 44 | archive: 'file.tar.gz', 45 | mode: 'compress', 46 | }), 47 | ).toBe('tar -czf file.tar.gz file') 48 | }) 49 | 50 | it('should support extract mode', () => { 51 | expect( 52 | formatTarCommand({ 53 | file: 'file', 54 | archive: 'file.tar.gz', 55 | mode: 'extract', 56 | }), 57 | ).toBe('tar -xzf file.tar.gz') 58 | }) 59 | 60 | it('should support "excludes"', () => { 61 | expect( 62 | formatTarCommand({ 63 | file: 'file', 64 | archive: 'file.tar.gz', 65 | mode: 'compress', 66 | excludes: ['foo'], 67 | }), 68 | ).toBe('tar --exclude "foo" -czf file.tar.gz file') 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/util.js: -------------------------------------------------------------------------------- 1 | export function escapeCommand(command) { 2 | return command.replace(/"/g, '\\"').replace(/\$/g, '\\$') 3 | } 4 | 5 | export function wrapCommand(command) { 6 | return `"${escapeCommand(command)}"` 7 | } 8 | 9 | export function joinCommandArgs(args) { 10 | return args.join(' ') 11 | } 12 | 13 | export function requireArgs(requiredArgs, args, command) { 14 | requiredArgs.forEach(required => { 15 | if (args[required] === undefined) { 16 | throw new Error( 17 | `"${required}" argument is required in "${command}" command`, 18 | ) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/commands/util.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | escapeCommand, 3 | joinCommandArgs, 4 | wrapCommand, 5 | requireArgs, 6 | } from './util' 7 | 8 | describe('util', () => { 9 | describe('#escapeCommand', () => { 10 | it('should escape double quotes', () => { 11 | expect(escapeCommand('echo "ok"')).toBe('echo \\"ok\\"') 12 | }) 13 | 14 | it('should escape $', () => { 15 | expect(escapeCommand('echo $FOO')).toBe('echo \\$FOO') 16 | }) 17 | }) 18 | 19 | describe('#wrapCommand', () => { 20 | it('should wrap command between double quotes', () => { 21 | expect(wrapCommand('echo "hello $USER"')).toBe( 22 | '"echo \\"hello \\$USER\\""', 23 | ) 24 | }) 25 | }) 26 | 27 | describe('#joinCommandArgs', () => { 28 | it('should join command args', () => { 29 | expect(joinCommandArgs(['echo', '"foo"'])).toBe('echo "foo"') 30 | }) 31 | }) 32 | 33 | describe('#requireArgs', () => { 34 | it('should require some args', () => { 35 | expect(() => requireArgs(['foo'], { a: 'b' }, 'custom')).toThrow( 36 | '"foo" argument is required in "custom" command', 37 | ) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/index.js: -------------------------------------------------------------------------------- 1 | import Connection from './Connection' 2 | import ConnectionPool from './ConnectionPool' 3 | import { exec } from './util' 4 | import { isRsyncSupported } from './commands/rsync' 5 | 6 | exports.Connection = Connection 7 | exports.ConnectionPool = ConnectionPool 8 | exports.exec = exec 9 | exports.isRsyncSupported = isRsyncSupported 10 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/remote.js: -------------------------------------------------------------------------------- 1 | import { deprecateV3 } from './util' 2 | 3 | export function parseRemote(remote) { 4 | if (remote && remote.host) return remote 5 | if (typeof remote !== 'string') throw new Error('A remote must be a string') 6 | if (remote === '') throw new Error('A remote cannot be an empty string') 7 | 8 | const matches = remote.match(/(([^@:]+)@)?([^@:]+)(:(.+))?/) 9 | 10 | if (matches) { 11 | const [, , user, host, , port] = matches 12 | const options = { user, host } 13 | if (port) options.port = Number(port) 14 | if (!user) { 15 | deprecateV3( 16 | 'Default user "deploy" is deprecated, please specify it explictly.', 17 | ) 18 | options.user = 'deploy' 19 | } 20 | return options 21 | } 22 | 23 | return { user: 'deploy', host: remote } 24 | } 25 | 26 | export function formatRemote({ user, host }) { 27 | return `${user}@${host}` 28 | } 29 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/remote.test.js: -------------------------------------------------------------------------------- 1 | import * as util from './util' 2 | import { parseRemote, formatRemote } from './remote' 3 | 4 | describe('SSH remote', () => { 5 | beforeEach(() => { 6 | util.deprecateV3 = jest.fn() 7 | }) 8 | 9 | describe('#parseRemote', () => { 10 | it('should return an error if not a string', () => { 11 | expect(() => { 12 | parseRemote({}) 13 | }).toThrow('A remote must be a string') 14 | }) 15 | 16 | it('should return an error if empty', () => { 17 | expect(() => { 18 | parseRemote('') 19 | }).toThrow('A remote cannot be an empty string') 20 | }) 21 | 22 | it('should return remote if it has an host', () => { 23 | const objRemote = { host: 'foo' } 24 | expect(parseRemote(objRemote)).toBe(objRemote) 25 | }) 26 | 27 | it('should use deploy as default user', () => { 28 | expect(parseRemote('host')).toEqual({ 29 | host: 'host', 30 | user: 'deploy', 31 | }) 32 | }) 33 | 34 | it('should parseRemote remote without port', () => { 35 | expect(parseRemote('user@host')).toEqual({ 36 | user: 'user', 37 | host: 'host', 38 | }) 39 | }) 40 | 41 | it('should parseRemote remote with port', () => { 42 | expect(parseRemote('user@host:300')).toEqual({ 43 | user: 'user', 44 | host: 'host', 45 | port: 300, 46 | }) 47 | }) 48 | }) 49 | 50 | describe('#format', () => { 51 | it('should format remote without port', () => { 52 | expect(formatRemote({ user: 'user', host: 'host' })).toBe('user@host') 53 | }) 54 | 55 | it('should format remote with port', () => { 56 | expect(formatRemote({ user: 'user', host: 'host', port: 3000 })).toBe( 57 | 'user@host', 58 | ) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/util.js: -------------------------------------------------------------------------------- 1 | import { exec as baseExec } from 'child_process' 2 | 3 | /* eslint-disable no-console */ 4 | export const series = async tasks => 5 | new Promise((resolve, reject) => { 6 | const tasksCopy = [...tasks] 7 | const next = results => { 8 | if (tasksCopy.length === 0) { 9 | resolve(results) 10 | return 11 | } 12 | const task = tasksCopy.shift() 13 | task() 14 | .then(result => next([...results, result])) 15 | .catch(reject) 16 | } 17 | next([]) 18 | }) 19 | 20 | const DEFAULT_CMD_OPTIONS = { maxBuffer: 1000 * 1024 } 21 | 22 | export const exec = async (cmd, options, childModifier) => 23 | new Promise((resolve, reject) => { 24 | const child = baseExec( 25 | cmd, 26 | { ...DEFAULT_CMD_OPTIONS, ...options }, 27 | (error, stdout, stderr) => { 28 | if (error) { 29 | /* eslint-disable no-param-reassign */ 30 | error.stdout = stdout 31 | error.stderr = stderr 32 | error.child = child 33 | /* eslint-enable no-param-reassign */ 34 | reject(error) 35 | } else { 36 | resolve({ child, stdout, stderr }) 37 | } 38 | }, 39 | ) 40 | 41 | if (childModifier) childModifier(child) 42 | }) 43 | 44 | export function deprecateV3(...args) { 45 | console.warn(...args, 'It will break in v3.0.0.') 46 | } 47 | 48 | export function deprecateV5(...args) { 49 | console.warn(...args, 'It will break in v5.0.0.') 50 | } 51 | -------------------------------------------------------------------------------- /packages/ssh-pool/src/util.test.js: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process' 2 | import { series, exec } from './util' 3 | 4 | jest.mock('child_process') 5 | 6 | describe('util', () => { 7 | describe('#series', () => { 8 | it('should run tasks in series', async () => { 9 | const results = await series([async () => 'foo', async () => 'bar']) 10 | expect(results).toEqual(['foo', 'bar']) 11 | }) 12 | 13 | it('should handle errors', async () => { 14 | expect.assertions(1) 15 | try { 16 | await series([ 17 | async () => { 18 | throw new Error('bad') 19 | }, 20 | async () => 'bar', 21 | ]) 22 | } catch (error) { 23 | expect(error.message).toBe('bad') 24 | } 25 | }) 26 | }) 27 | 28 | describe('#exec', () => { 29 | it('should accept a childModifier', () => { 30 | const childModifier = jest.fn() 31 | exec('test', { foo: 'bar' }, childModifier) 32 | expect(childModifier).toBeCalled() 33 | }) 34 | 35 | it('should return child, stdout and stderr', async () => { 36 | const result = await exec('test', { foo: 'bar' }) 37 | expect(result.child).toBeDefined() 38 | expect(result.stdout).toBeDefined() 39 | expect(result.stderr).toBeDefined() 40 | }) 41 | 42 | it('should return child, stdout and stderr', async () => { 43 | childProcess.exec = jest.fn((command, options, callback) => { 44 | setTimeout(() => callback(new Error('Oups'), 'stdout', 'stderr')) 45 | return 'child' 46 | }) 47 | expect.assertions(4) 48 | try { 49 | await exec('test', { foo: 'bar' }) 50 | } catch (error) { 51 | expect(error.child).toBeDefined() 52 | expect(error.stdout).toBeDefined() 53 | expect(error.stderr).toBeDefined() 54 | expect(error.message).toBe('Oups') 55 | } 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/ssh-pool/tests/__fixtures__/test.txt: -------------------------------------------------------------------------------- 1 | Hello 2 | -------------------------------------------------------------------------------- /packages/ssh-pool/tests/integration.test.js: -------------------------------------------------------------------------------- 1 | import {resolve, basename} from 'path' 2 | import {copyFileSync, unlinkSync} from 'fs'; 3 | 4 | const sshPool = require('../src') 5 | 6 | describe('ssh-pool', () => { 7 | let pool 8 | 9 | beforeEach(() => { 10 | pool = new sshPool.ConnectionPool(['deploy@test.shipitjs.com'], { 11 | key: resolve(__dirname, '../../../ssh/id_rsa'), 12 | }) 13 | }) 14 | 15 | it('should run a command remotely', async () => { 16 | const [{ stdout }] = await pool.run('hostname') 17 | expect(stdout).toBe('shipit-test\n') 18 | }, 10000) 19 | 20 | it('should escape command properly', async () => { 21 | const [{ stdout: first }] = await pool.run('echo $USER') 22 | expect(first).toBe('deploy\n') 23 | 24 | const [{ stdout: second }] = await pool.run("echo '$USER'") 25 | expect(second).toBe('$USER\n') 26 | }, 10000) 27 | 28 | it('should copy to remote', async () => { 29 | const time = (+new Date); 30 | const sourceFile = resolve(__dirname, '__fixtures__/test.txt') 31 | const targetFile = `${__dirname}/__fixtures__/test.${time}.txt`; 32 | 33 | copyFileSync(sourceFile, targetFile); 34 | 35 | try { 36 | await pool.scpCopyToRemote(targetFile, './',); 37 | const [{ stdout: first }] = await pool.run(`cd ./ && cat ${basename(targetFile)}`); 38 | expect(first).toBe('Hello\n') 39 | } finally { 40 | unlinkSync(targetFile); 41 | } 42 | 43 | }, 1e6) 44 | }) 45 | -------------------------------------------------------------------------------- /resources/shipit-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipitjs/shipit/5e9b5fb4396e75cbe43382781c0b06491a66ad3c/resources/shipit-logo-dark.png -------------------------------------------------------------------------------- /resources/shipit-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipitjs/shipit/5e9b5fb4396e75cbe43382781c0b06491a66ad3c/resources/shipit-logo-light.png -------------------------------------------------------------------------------- /resources/shipit-logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipitjs/shipit/5e9b5fb4396e75cbe43382781c0b06491a66ad3c/resources/shipit-logo.sketch -------------------------------------------------------------------------------- /ssh/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAvhm/0j4Hzm0wMZKRri3Nd+fmApLThXuyriR+CqiEONkXNNgt 3 | 7B6usy65EhAPpByo58vNrYeIkBAdzWrtt5j1blQiYfZYhKcGHrBhjbK8WNBnHuG5 4 | FepD04tZaaHjlzJ9J1zayKtU1AiZjl8pKmnU7n6Jo6sGhgYcPilUBmPfB0nFlvr0 5 | uc4xUrWg2Fl0h3Ww1JlNhnMfLK/wOjaSHLgNw6yiixyG1TJAcrlzNJ1pHXFExAHw 6 | 7j2nBcgwDUCNLgAnMWFN8FB6ibN6CFt7NCWtOLueAqU7FphwurBqCqcRH+gyOF23 7 | ZtmiupsyzCojTniZ0PpMWrDWgZ9aAqA+1BbfyQIDAQABAoIBAQCRCHQgotKx2vv5 8 | 1ijvCmLIKFSDgiF+pXEdCxpeZ1L5TCc4WfYvPvlqGyt3bGmCe5shvYud6Nl3j9Qs 9 | 9HeIq1oUYnwY4SmHiyZQI6FJyiOIXvdNyEi9P42fx6DfxnMs14hEj8Mbdhux6R2+ 10 | UTvG8BdUHZZFGCZR+jdx9XX1qhxuIbiwzQG4979FjDnGAQ5AG5rEGXIDOTt7UuWl 11 | ZDLpUtiO6LD9tJt47oO6IHOX1u4uOGz1yYCkjyjdTL0oGaEHaiaVTJ10QsjPoGrL 12 | Wh+T7EXPxALClnhMfyjc4XfnnU5YSm3/yEeG2N6j4kcvw2/g/jm74ONulAEMVZDK 13 | fC+Y3NVBAoGBAO9lRDNEAw4317tBs/9L65E/yf32o/wSN8tRf/dgO89CJzvX2SFx 14 | w5Jw4ULo1QphRETJ3iWD7LL2lpY60VVijzCyg1hTjdD9BJQXgEnTgJI5qZyiJrt6 15 | rp0j+gyGy/XImbl9PnRdvaWdjqhczJdiNyJYxdnpxubJKrxmOXPt0ZqDAoGBAMtJ 16 | Mh1DCVqpGlt1va852caeIItMac8Ht8qCQ0zsyeUKUjOhny5uxKGC2NsOiNnqlzFi 17 | Txjr6igtuKInL9jYFOUhoC16MF5AsjQdO0+dslOflU9ptC6oKcdSLKIeFaYqsna2 18 | a6fiMf9CkEsZtGIz8ggBsbTMi1Iazr4NDphUObrDAoGBAMj7fsd/iQUt0ttubNyf 19 | 85SNNlsV71SYQulachHQZEY75s5yB+PxK91NEYFoEjvVr0gFJpDeciFJruFPXiHO 20 | TiL3LBhChaR4V5ixJk5U1/Nrn79VzyjE9cYNx0cvABtIH+8/e+icLrTVU0h8KHPL 21 | zDf0yZ6KiyeEqnFjbUar2bZbAoGBAK3XDlAPv7QT4EJOUcPDCQTcvJ/i3Kj6xKUc 22 | +EiURaLkTJ9ymxmuB+DGcIQDzevsvRayJ0n8lOV/E+E2+afKQTQgqUW6tBol4T7H 23 | sKzJAnKYiaq7jiZIEFIvZ5PLfl/3K15xaWbL/E15ssNGXAeOvG80Y69lK88utZW4 24 | vL5vaF7ZAoGAfoH1IMgYmZOK5M6/nSNtfEciPrEMBwEsQGXOhtdVf6Ul6OmScEU8 25 | BAOcwiWjUr1iS3Y9eYce6t4F3ZZf9zSuA5eaXuRkeJaOwFMwklNuezNGY1Y6ZVgH 26 | Syw/+CXag6aWwa+43qVFON1t5G5GXKvnI+W0FyTKleOENe3LSzAvrW4= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /ssh/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+Gb/SPgfObTAxkpGuLc135+YCktOFe7KuJH4KqIQ42Rc02C3sHq6zLrkSEA+kHKjny82th4iQEB3Nau23mPVuVCJh9liEpwYesGGNsrxY0Gce4bkV6kPTi1lpoeOXMn0nXNrIq1TUCJmOXykqadTufomjqwaGBhw+KVQGY98HScWW+vS5zjFStaDYWXSHdbDUmU2Gcx8sr/A6NpIcuA3DrKKLHIbVMkByuXM0nWkdcUTEAfDuPacFyDANQI0uACcxYU3wUHqJs3oIW3s0Ja04u54CpTsWmHC6sGoKpxEf6DI4Xbdm2aK6mzLMKiNOeJnQ+kxasNaBn1oCoD7UFt/J shipit-test 2 | --------------------------------------------------------------------------------