├── .editorconfig ├── .ember-cli ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── index.js ├── lib └── ssh-client.js ├── package.json └── tests ├── .jshintrc ├── fixtures └── dist │ ├── index.html │ └── manifest.appcache ├── helpers └── assert.js ├── jshint.spec.js ├── runner.js └── unit ├── .gitkeep ├── index-nodetest.js └── lib └── ssh-client-nodetest.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log* 17 | testem.log 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esversion": 6, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .gitignore 11 | .jshintrc 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "10" 5 | 6 | sudo: false 7 | 8 | 9 | install: 10 | - npm install 11 | 12 | script: 13 | - npm test 14 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.0.7] - 2019-05-11 4 | - [Pull Request #16](https://github.com/arenoir/ember-cli-deploy-ssh2/pull/16) Close sftp channels/connections 5 | 6 | ## [0.0.6] - 2017-04-21 7 | - [Pull Request #14](https://github.com/arenoir/ember-cli-deploy-ssh2/pull/14) Fix ember-cli/ext/promise Deprecation for Ember CLI >=2.12.0 8 | 9 | ## [0.0.5] - 2017-03-01 10 | - [Pull Request #13](https://github.com/arenoir/ember-cli-deploy-ssh2/pull/13) Make sure _fetchRevisionManifest works if the manifest is empty 11 | - [Pull Request #12](https://github.com/arenoir/ember-cli-deploy-ssh2/pull/12) update ember cli 12 | 13 | ## [0.0.4] - 2016-07-14 14 | - change copy strategy flags. 15 | 16 | ## [0.0.3] - 2016-05-02 17 | - Add activationStrategy to config options. Because nginx on alpine linux wasn't following a symlink from a alias directive. 18 | 19 | ## [0.0.2] - 2016-04-26 20 | - Update ssh2 module thus dropping support for node v0.8 21 | - [Issue #5](https://github.com/arenoir/ember-cli-deploy-ssh2/issues/5) Add passphrase to config options. 22 | 23 | ## [0.0.1] - 2015-11-24 24 | - Return revisionData object from activate hook. Useful for notification plugins. 25 | 26 | ## [0.0.0] - 2015-11-21 27 | - Initial release 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-cli-deploy-ssh2 [![Build Status](https://travis-ci.org/arenoir/ember-cli-deploy-ssh2.svg?branch=master)](https://travis-ci.org/arenoir/ember-cli-deploy-ssh2) 2 | 3 | > An ember-cli-deploy plugin to upload activate and list versioned application file/s using ssh. 4 | 5 |
6 | **WARNING: This plugin is only compatible with ember-cli-deploy versions >= 0.5.0** 7 |
8 | 9 | 10 | This plugin uploads, activates and lists deployed revisions. It is different from other plugins as it works with multiple `applicationFiles`. The primary use case is being able to keep the `index.html` and `manifest.appcache` files versioned and activated together. 11 | 12 | 13 | ## Quick Start 14 | To get up and running quickly, do the following: 15 | 16 | - Ensure [ember-cli-deploy-build][1], [ember-cli-deploy-revision-data][3] and [ember-cli-deploy-display-revisions][4]) are installed and configured. 17 | 18 | - Install this plugin 19 | 20 | ```bash 21 | $ ember install ember-cli-deploy-ssh2 22 | ``` 23 | 24 | - Place the following configuration into `config/deploy.js` 25 | 26 | ```javascript 27 | ENV['ssh2'] = { 28 | host: 'webserver1.example.com', 29 | username: 'production-deployer', 30 | privateKeyPath: '~/.ssh/id_rsa', 31 | port: 22, 32 | applicationFiles: ['index.html', 'manifest.appcache'], 33 | root: '/usr/local/www/my-application' 34 | } 35 | ``` 36 | 37 | - Run the pipeline 38 | 39 | ```bash 40 | $ ember deploy 41 | ``` 42 | 43 | ## Configuration Options 44 | 45 | ### host 46 | The host name or ip address of the machine to connet to. 47 | 48 | *Default:* `''` 49 | 50 | ### username 51 | 52 | The username to use to open a ssh connection. 53 | 54 | *Default:* `''` 55 | 56 | ### privateKeyPath 57 | 58 | The path to a private key to authenticate the ssh connection. 59 | 60 | *Default:* ```'~/.ssh/id_rsa'``` 61 | 62 | ### passphrase 63 | 64 | The passphrase used to decrypt the privateKey. 65 | 66 | *Default:* ```none``` 67 | 68 | ### port 69 | The port to connect on. 70 | 71 | *Default:* ```'22'``` 72 | 73 | ### applicationFiles 74 | A list of files to upload to the server. 75 | 76 | *Default:* ```['index.html']``` 77 | 78 | ### root 79 | 80 | A function or string used to determine where to upload `applicationFiles`. 81 | 82 | *Note:* ```This directory will not be created it must exist on server.`` 83 | 84 | *Default:* ```'/usr/local/www/' + context.project.name()``` 85 | 86 | ### uploadDestination 87 | 88 | A string or a function returning the path where the application files are stored. 89 | 90 | *Default:* 91 | ``` 92 | function(context){ 93 | return path.join(this.readConfig('root'), 'revisions'); 94 | } 95 | ``` 96 | 97 | ### activationDestination 98 | 99 | The path that the active version should be linked to. 100 | 101 | *Default:* 102 | ``` 103 | function(context) { 104 | return path.join(this.readConfig('root'), 'active'); 105 | } 106 | ``` 107 | 108 | ### activationStrategy 109 | 110 | How revisions are activated either by symlink or copying revision directory. 111 | 112 | *Default:* ```"symlink"``` 113 | 114 | 115 | ### revisionManifest 116 | 117 | A string or a function returning the path where the revision manifest is located. 118 | 119 | *Default:* 120 | ``` 121 | function(context) { 122 | return path.join(this.readConfig('root'), 'revisions.json'); 123 | } 124 | ``` 125 | 126 | ### revisionMeta 127 | A function returning a hash of meta data to include with the revision. 128 | 129 | *Default:* 130 | ``` 131 | function(context) { 132 | var revisionKey = this.readConfig('revisionKey'); 133 | var who = username.sync() + '@' + os.hostname(); 134 | 135 | return { 136 | revision: revisionKey, 137 | deployer: who, 138 | timestamp: new Date().getTime(), 139 | } 140 | } 141 | ``` 142 | 143 | 144 | ## Prerequisites 145 | 146 | The following properties are expected to be present on the deployment `context` object: 147 | 148 | - `distDir` (provided by [ember-cli-deploy-build][2]) 149 | - `revisionData` (provided by [ember-cli-deploy-revision-data][3]) 150 | 151 | The following commands require: 152 | 153 | - `deploy:list` (provided by [ember-cli-deploy-display-revisions][4]) 154 | 155 | 156 | 157 | [1]: http://ember-cli.github.io/ember-cli-deploy/plugins "Plugin Documentation" 158 | [2]: https://github.com/ember-cli-deploy/ember-cli-deploy-build "ember-cli-deploy-build" 159 | [3]: https://github.com/ember-cli-deploy/ember-cli-deploy-revision-data "ember-cli-deploy-revision-data" 160 | [4]: https://github.com/ember-cli-deploy/ember-cli-deploy-display-revisions "ember-cli-deploy-display-revisions" 161 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var Promise = require('rsvp').Promise; 5 | var DeployPluginBase = require('ember-cli-deploy-plugin'); 6 | var path = require('path'); 7 | var os = require('os'); 8 | var username = require('username'); 9 | var lodash = require('lodash'); 10 | var sshClient = require('./lib/ssh-client'); 11 | 12 | module.exports = { 13 | name: 'ember-cli-deploy-ssh2', 14 | 15 | createDeployPlugin: function(options) { 16 | var DeployPlugin = DeployPluginBase.extend({ 17 | name: options.name, 18 | _sshClient: sshClient, 19 | _client: null, 20 | defaultConfig: { 21 | distDir: function(context) { 22 | return context.distDir; 23 | }, 24 | host: '', 25 | username: '', 26 | password: null, 27 | privateKeyPath: '~/.ssh/id_rsa', 28 | agent: null, 29 | port: 22, 30 | applicationFiles: ['index.html'], 31 | 32 | root: function(context) { 33 | return path.posix.join('/usr/local/www', context.project.name()); 34 | }, 35 | 36 | activationDestination: function(context, pluginHelper) { 37 | var root = pluginHelper.readConfig('root'); 38 | 39 | return path.posix.join(root, 'active'); 40 | }, 41 | 42 | activationStrategy: 'symlink', 43 | 44 | uploadDestination: function(context, pluginHelper){ 45 | var root = pluginHelper.readConfig('root'); 46 | 47 | return path.posix.join(root, 'revisions'); 48 | }, 49 | 50 | revisionManifest: function(context, pluginHelper) { 51 | var root = pluginHelper.readConfig('root'); 52 | 53 | return path.posix.join(root, 'revisions.json'); 54 | }, 55 | 56 | revisionKey: function(context) { 57 | return (context.commandOptions && context.commandOptions.revision) || (context.revisionData && context.revisionData.revisionKey); 58 | }, 59 | 60 | revisionMeta: function(context, pluginHelper) { 61 | var revisionKey = pluginHelper.readConfig('revisionKey'); 62 | var who = username.sync() + '@' + os.hostname(); 63 | 64 | return { 65 | revision: revisionKey, 66 | deployer: who, 67 | timestamp: new Date().getTime(), 68 | }; 69 | }, 70 | }, 71 | 72 | configure: function(context) { 73 | this._super.configure.call(this, context); 74 | 75 | var options = { 76 | host: this.readConfig('host'), 77 | username: this.readConfig('username'), 78 | password: this.readConfig('password'), 79 | port: this.readConfig('port'), 80 | privateKeyPath: this.readConfig('privateKeyPath'), 81 | passphrase: this.readConfig('passphrase'), 82 | agent: this.readConfig('agent') 83 | }; 84 | 85 | this._client = new this._sshClient(options); 86 | return this._client.connect(this); 87 | }, 88 | 89 | activate: function(/*context*/) { 90 | var _this = this; 91 | var client = this._client; 92 | var revisionKey = this.readConfig('revisionKey'); 93 | var activationDestination = this.readConfig('activationDestination'); 94 | var uploadDestination = path.posix.join(this.readConfig('uploadDestination'), '/'); 95 | var activeRevisionPath = path.posix.join(uploadDestination, revisionKey, '/'); 96 | var activationStrategy = this.readConfig('activationStrategy'); 97 | var revisionData = { 98 | revisionData: { 99 | activatedRevisionKey: revisionKey 100 | } 101 | }; 102 | var linkCmd; 103 | 104 | this.log('Activating revision ' + revisionKey); 105 | 106 | if (activationStrategy === "copy") { 107 | linkCmd = 'cp -TR ' + activeRevisionPath + ' ' + activationDestination; 108 | } else { 109 | linkCmd = 'ln -fsn ' + activeRevisionPath + ' ' + activationDestination; 110 | } 111 | 112 | return client 113 | .exec(linkCmd) 114 | .then(function() { 115 | return _this 116 | ._activateRevisionManifest() 117 | .then(function() { 118 | return revisionData; 119 | }); 120 | }); 121 | }, 122 | 123 | fetchRevisions: function(context) { 124 | this.log('Fetching Revisions'); 125 | 126 | return this._fetchRevisionManifest().then(function(manifest) { 127 | context.revisions = manifest; 128 | }); 129 | }, 130 | 131 | upload: function(/*context*/) { 132 | var _this = this; 133 | 134 | return this._updateRevisionManifest().then(function() { 135 | _this.log('Successfully uploaded updated manifest.', {verbose: true}); 136 | 137 | return _this._uploadApplicationFiles(); 138 | }); 139 | }, 140 | 141 | teardown: function(/*context*/) { 142 | this.log('Teardown - closing sftp connection.', { verbose: true }); 143 | 144 | return this._client.disconnect(); 145 | }, 146 | 147 | _uploadApplicationFiles: function(/*context*/) { 148 | var client = this._client; 149 | var files = this.readConfig('applicationFiles'); 150 | var distDir = this.readConfig('distDir'); 151 | var revisionKey = this.readConfig('revisionKey'); 152 | var uploadDestination = this.readConfig('uploadDestination'); 153 | var destination = path.posix.join(uploadDestination, revisionKey); 154 | var _this = this; 155 | 156 | this.log('Uploading `applicationFiles` to ' + destination); 157 | 158 | var promises = []; 159 | files.forEach(function(file) { 160 | var src = path.join(distDir, file); 161 | var dest = path.posix.join(destination, file); 162 | 163 | promises.push(client.putFile(src, dest)); 164 | }); 165 | 166 | return Promise.all(promises).then( 167 | function() { 168 | _this.log('Successfully uploaded file/s.', { color: 'green' }); 169 | }, 170 | function() { 171 | _this.log('Failed to upload file/s.', { color: 'red' }); 172 | } 173 | ); 174 | }, 175 | 176 | _activateRevisionManifest: function(/*context*/) { 177 | var _this = this; 178 | var revisionKey = this.readConfig('revisionKey'); 179 | var manifestPath = this.readConfig('revisionManifest'); 180 | 181 | return this._fetchRevisionManifest().then( 182 | function(manifest) { 183 | manifest = lodash.map(manifest, function(rev) { 184 | if (rev.revision = revisionKey) { 185 | rev.active = true; 186 | } else { 187 | delete rev['active']; 188 | } 189 | 190 | return rev; 191 | }); 192 | 193 | return _this._uploadRevisionManifest(manifestPath, manifest); 194 | }, 195 | function(error) { 196 | _this.log(error.message, {color: 'red'}); 197 | } 198 | ); 199 | }, 200 | 201 | _updateRevisionManifest: function() { 202 | var revisionKey = this.readConfig('revisionKey'); 203 | var revisionMeta = this.readConfig('revisionMeta'); 204 | var manifestPath = this.readConfig('revisionManifest'); 205 | var _this = this; 206 | 207 | this.log('Updating `revisionManifest` ' + manifestPath, {verbose: true}); 208 | 209 | return this._fetchRevisionManifest().then( 210 | function(manifest) { 211 | var existing = manifest.some(function(rev) { 212 | return rev.revision === revisionKey; 213 | }); 214 | 215 | if (existing) { 216 | _this.log('Revision ' + revisionKey + ' already added to `revisionManifest` moving on.', {verbose: true}); 217 | return; 218 | } 219 | 220 | _this.log('Adding ' + JSON.stringify(revisionMeta), {verbose: true}); 221 | 222 | manifest.unshift(revisionMeta); 223 | 224 | return _this._uploadRevisionManifest(manifestPath, manifest); 225 | }, 226 | function(error) { 227 | _this.log(error.message, {color: 'red'}); 228 | } 229 | ); 230 | }, 231 | 232 | _fetchRevisionManifest: function() { 233 | var manifestPath = this.readConfig('revisionManifest'); 234 | var client = this._client; 235 | var _this = this; 236 | 237 | return client.readFile(manifestPath).then( 238 | function(manifest) { 239 | _this.log('fetched manifest ' + manifestPath, {verbose: true}); 240 | 241 | return lodash.isEmpty(manifest) ? [] : JSON.parse(manifest); 242 | }, 243 | function(error) { 244 | if (error.message === "No such file") { 245 | _this.log('Revision manifest not present building new one.', {verbose: true}); 246 | 247 | return []; 248 | } else { 249 | _this.log(error.message, {color: 'red'}); 250 | } 251 | } 252 | ); 253 | }, 254 | 255 | _uploadRevisionManifest: function(manifestPath, manifest) { 256 | var data = new Buffer(JSON.stringify(manifest), "utf-8"); 257 | 258 | return this._client.upload(manifestPath, data); 259 | } 260 | }); 261 | 262 | return new DeployPlugin(); 263 | } 264 | }; 265 | -------------------------------------------------------------------------------- /lib/ssh-client.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var CoreObject = require('core-object'); 5 | var Promise = require('rsvp').Promise; 6 | var SSH2Client = require('ssh2').Client; 7 | var fs = require('fs'); 8 | var untildify = require('untildify'); 9 | 10 | 11 | module.exports = CoreObject.extend({ 12 | 13 | init: function(options) { 14 | if (options.agent) { 15 | delete options["privateKeyPath"]; 16 | } 17 | 18 | if (options.privateKeyPath) { 19 | options.privateKey = fs.readFileSync(untildify(options.privateKeyPath)); 20 | } 21 | 22 | this.options = options; 23 | this.client = new SSH2Client(); 24 | }, 25 | 26 | 27 | connect: function() { 28 | var client = this.client; 29 | var options = this.options; 30 | 31 | client.connect(options); 32 | 33 | return new Promise(function(resolve, reject) { 34 | client.on('error', reject); 35 | client.on('ready', resolve); 36 | }); 37 | }, 38 | 39 | disconnect: function() { 40 | var client = this.client; 41 | 42 | client.end(); 43 | 44 | return new Promise(function(resolve, reject) { 45 | client.on('error', reject); 46 | client.on('end', resolve); 47 | }); 48 | }, 49 | 50 | upload: function(path, data) { 51 | var client = this.client; 52 | 53 | return new Promise(function (resolve, reject) { 54 | client.sftp(function(error, sftp) { 55 | if (error) { 56 | reject(error); 57 | } 58 | 59 | var stream = sftp.createWriteStream(path); 60 | 61 | stream.on('error', reject); 62 | stream.on('finish', resolve); 63 | stream.on('close', function() { 64 | sftp.end(); 65 | }); 66 | stream.write(data); 67 | stream.end(); 68 | }); 69 | }); 70 | }, 71 | 72 | 73 | readFile: function(path) { 74 | var client = this.client; 75 | 76 | return new Promise(function(resolve, reject) { 77 | client.sftp(function(error, sftp) { 78 | if (error) { 79 | reject(error); 80 | } 81 | 82 | sftp.readFile(path, {}, function (error, data) { 83 | sftp.end(); 84 | 85 | if (error) { 86 | reject(error); 87 | } else { 88 | resolve(data); 89 | } 90 | }); 91 | }); 92 | }); 93 | }, 94 | 95 | 96 | exec: function(command) { 97 | var client = this.client; 98 | 99 | return new Promise(function(resolve, reject) { 100 | client.exec(command, function(error/*, stream*/) { 101 | if (error) { 102 | reject(error); 103 | } 104 | resolve(); 105 | }); 106 | }); 107 | }, 108 | 109 | 110 | putFile: function(src, dest) { 111 | var _this = this; 112 | var client = this.client; 113 | 114 | return new Promise(function(resolve, reject) { 115 | var parts = dest.split('/'); 116 | parts.pop(); 117 | var destdir = parts.join('/'); 118 | var scpcmd = 'mkdir -p ' + destdir; 119 | 120 | _this.exec(scpcmd).then( 121 | function() { 122 | client.sftp(function (err, sftp) { 123 | if (err) { 124 | reject(err); 125 | } 126 | 127 | sftp.fastPut(src, dest, {}, function (err) { 128 | sftp.end(); 129 | 130 | if (err) { 131 | reject(err); 132 | } 133 | resolve(); 134 | }); 135 | }); 136 | }, 137 | reject 138 | ); 139 | }); 140 | } 141 | }); 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-cli-deploy-ssh2", 3 | "version": "0.0.7", 4 | "description": "An ember-cli-deploy plugin to upload activate and list versioned application file/s using ssh.", 5 | "keywords": [ 6 | "ember-addon", 7 | "ember-cli-deploy-plugin" 8 | ], 9 | "license": "MIT", 10 | "author": "Aaron Renoir", 11 | "directories": { 12 | "doc": "doc", 13 | "test": "tests" 14 | }, 15 | "repository": "https://github.com/arenoir/ember-cli-deploy-ssh2", 16 | "scripts": { 17 | "test": "node tests/runner.js" 18 | }, 19 | "dependencies": { 20 | "broccoli-asset-rewrite": "^1.0.12", 21 | "chalk": "^1.1.3", 22 | "core-object": "^1.1.0", 23 | "ember-cli-deploy-plugin": "^0.2.9", 24 | "lodash": "^4.17.15", 25 | "rsvp": "^4.8.5", 26 | "ssh2": "~0.8.9", 27 | "untildify": "^4.0.0", 28 | "username": "^5.1.0" 29 | }, 30 | "devDependencies": { 31 | "chai": "^3.4.1", 32 | "chai-as-promised": "^5.1.0", 33 | "ember-cli": "^3.16.0", 34 | "glob": "^7.1.6", 35 | "mocha": "^6.2.3", 36 | "mocha-jshint": "^2.3.1", 37 | "multiline": "^1.0.2" 38 | }, 39 | "engines": { 40 | "node": "6.* || 8.* || >= 10.*" 41 | }, 42 | "ember-addon": { 43 | "configPath": "tests/dummy/config" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "location", 6 | "setTimeout", 7 | "$", 8 | "Promise", 9 | "define", 10 | "console", 11 | "visit", 12 | "exists", 13 | "fillIn", 14 | "click", 15 | "keyEvent", 16 | "triggerEvent", 17 | "find", 18 | "findWithAssert", 19 | "wait", 20 | "DS", 21 | "andThen", 22 | "currentURL", 23 | "currentPath", 24 | "currentRouteName", 25 | "describe", 26 | "before", 27 | "beforeEach", 28 | "it" 29 | ], 30 | "node": true, 31 | "browser": false, 32 | "boss": true, 33 | "curly": true, 34 | "debug": false, 35 | "devel": false, 36 | "eqeqeq": true, 37 | "evil": true, 38 | "forin": false, 39 | "immed": false, 40 | "laxbreak": false, 41 | "newcap": true, 42 | "noarg": true, 43 | "noempty": false, 44 | "nonew": false, 45 | "nomen": false, 46 | "onevar": false, 47 | "plusplus": false, 48 | "regexp": false, 49 | "undef": true, 50 | "sub": true, 51 | "strict": false, 52 | "white": false, 53 | "eqnull": true, 54 | "esversion": 6, 55 | "unused": true 56 | } 57 | -------------------------------------------------------------------------------- /tests/fixtures/dist/index.html: -------------------------------------------------------------------------------- 1 | indexpage -------------------------------------------------------------------------------- /tests/fixtures/dist/manifest.appcache: -------------------------------------------------------------------------------- 1 | manifestfile -------------------------------------------------------------------------------- /tests/helpers/assert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | 8 | module.exports = chai.assert; -------------------------------------------------------------------------------- /tests/jshint.spec.js: -------------------------------------------------------------------------------- 1 | require('mocha-jshint')({ 2 | paths: [ 3 | 'index.js', 4 | 'lib', 5 | 'tests' 6 | ] 7 | }); -------------------------------------------------------------------------------- /tests/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var glob = require('glob'); 4 | var Mocha = require('mocha'); 5 | 6 | var mocha = new Mocha({ 7 | reporter: 'spec' 8 | }); 9 | 10 | 11 | var arg = process.argv[2]; 12 | var root = 'tests/'; 13 | 14 | function addFiles(mocha, files) { 15 | glob.sync(root + files).forEach(mocha.addFile.bind(mocha)); 16 | } 17 | 18 | addFiles(mocha, 'jshint.spec.js'); 19 | 20 | 21 | addFiles(mocha, '/**/*-nodetest.js'); 22 | 23 | 24 | if (arg === 'all') { 25 | addFiles(mocha, '/**/*-nodetest-slow.js'); 26 | } 27 | 28 | 29 | mocha.run(function(failures) { 30 | process.on('exit', function() { 31 | process.exit(failures); 32 | }); 33 | }); -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arenoir/ember-cli-deploy-ssh2/0b263dbb87af36150af17204be62d4191a75d5c9/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/index-nodetest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var assert = require('../helpers/assert'); 5 | var CoreObject = require('core-object'); 6 | 7 | var mockSSHClient = CoreObject.extend({ 8 | init: function(options) { 9 | this.options = options; 10 | }, 11 | 12 | connect: function() { 13 | this._connected = true; 14 | return Promise.resolve(); 15 | }, 16 | 17 | _readFileError: null, 18 | 19 | readFile: function(path) { 20 | var readFileError = this._readFileError; 21 | var uploads = this._uploadedFiles; 22 | 23 | return new Promise(function(resolve, reject) { 24 | if (readFileError) { 25 | reject(readFileError); 26 | } else { 27 | var file = uploads[path]; 28 | resolve(file); 29 | } 30 | }); 31 | }, 32 | 33 | _uploadedFiles: {}, 34 | 35 | upload: function(path, data) { 36 | var files = this._uploadedFiles; 37 | 38 | return new Promise(function(resolve) { 39 | files[path] = data.toString(); 40 | resolve(); 41 | }); 42 | }, 43 | 44 | putFile: function(src, dest) { 45 | var files = this._uploadedFiles; 46 | 47 | var file = fs.readFileSync(src, "utf8"); 48 | 49 | return new Promise(function(resolve) { 50 | files[dest] = file.toString(); 51 | resolve(); 52 | }); 53 | }, 54 | 55 | _command: '', 56 | exec: function(command) { 57 | var _this = this; 58 | return new Promise(function(resolve) { 59 | _this._command = command; 60 | resolve(); 61 | }); 62 | }, 63 | }); 64 | 65 | 66 | describe('the deploy plugin object', function() { 67 | var plugin; 68 | var configure; 69 | var context; 70 | 71 | before(function() { 72 | 73 | }); 74 | 75 | beforeEach(function() { 76 | var subject = require('../../index'); 77 | 78 | plugin = subject.createDeployPlugin({ 79 | name: 'ssh2', 80 | }); 81 | 82 | context = { 83 | ui: {write: function() {}, writeLine: function() {}}, 84 | config: { 85 | 'ssh2': { 86 | username: 'deployer', 87 | password: 'mypass', 88 | applicationFiles: ['index.html', 'manifest.appcache'], 89 | root: '/usr/local/www/my-app', 90 | distDir: 'tests/fixtures/dist', 91 | revisionMeta: function(context, pluginHelper) { 92 | var revisionKey = pluginHelper.readConfig('revisionKey'); 93 | 94 | return { 95 | revision: revisionKey, 96 | }; 97 | }, 98 | } 99 | }, 100 | revisionData: { 101 | revisionKey: '89b1d82820a24bfb075c5b43b36f454b' 102 | } 103 | }; 104 | 105 | plugin._sshClient = mockSSHClient; 106 | 107 | plugin.beforeHook(context); 108 | configure = plugin.configure(context); 109 | }); 110 | 111 | it('has a name', function() { 112 | assert.equal('ssh2', plugin.name); 113 | }); 114 | 115 | it('implements the correct hooks', function() { 116 | assert.equal(typeof plugin.configure, 'function'); 117 | assert.equal(typeof plugin.fetchRevisions, 'function'); 118 | }); 119 | 120 | describe('configure hook', function() { 121 | it('opens up a ssh connection.', function() { 122 | return assert.isFulfilled(configure) 123 | .then(function() { 124 | var client = plugin._client; 125 | 126 | assert.equal(client._connected, true); 127 | }); 128 | }); 129 | 130 | it('instantiates a sshClient and assigns it to the `_client` property.', function() { 131 | return assert.isFulfilled(configure) 132 | .then(function() { 133 | var client = plugin._client; 134 | 135 | assert.equal(client.options.username, "deployer"); 136 | assert.equal(client.options.password, "mypass"); 137 | }); 138 | }); 139 | }); 140 | 141 | describe('fetchRevisions hook', function() { 142 | it('assigins context.revisions property.', function() { 143 | var revisions = [{"revision": "4564564545646"}]; 144 | var client = plugin._client; 145 | var files = {}; 146 | 147 | files["/usr/local/www/my-app/revisions.json"] = JSON.stringify(revisions); 148 | 149 | client._uploadedFiles = files; 150 | 151 | var fetching = plugin.fetchRevisions(context); 152 | 153 | return assert.isFulfilled(fetching).then(function() { 154 | assert.deepEqual(context.revisions, revisions); 155 | }); 156 | }); 157 | 158 | it('assigins context.revisions proptery to empty array if revistion file not found.', function() { 159 | var client = plugin._client; 160 | 161 | client._readFileError = new Error('No such file'); 162 | client._readFile = null; 163 | 164 | var fetching = plugin.fetchRevisions(context); 165 | 166 | return assert.isFulfilled(fetching).then(function() { 167 | assert.deepEqual(context.revisions, []); 168 | }); 169 | }); 170 | }); 171 | 172 | describe('activate hook', function() { 173 | it('creates a symbolic link to active version', function() { 174 | var activating = plugin.activate(context); 175 | var client = plugin._client; 176 | 177 | return assert.isFulfilled(activating).then(function() { 178 | assert.equal(client._command, 'ln -fsn /usr/local/www/my-app/revisions/89b1d82820a24bfb075c5b43b36f454b/ /usr/local/www/my-app/active'); 179 | }); 180 | }); 181 | 182 | it('copies revision to activationDestination if activationStrategy is copy', function() { 183 | context.config.ssh2.activationStrategy = "copy"; 184 | plugin.configure(context); 185 | 186 | var activating = plugin.activate(context); 187 | var client = plugin._client; 188 | 189 | return assert.isFulfilled(activating).then(function() { 190 | assert.equal(client._command, 'cp -TR /usr/local/www/my-app/revisions/89b1d82820a24bfb075c5b43b36f454b/ /usr/local/www/my-app/active'); 191 | }); 192 | }); 193 | 194 | it('returns revisionData', function() { 195 | var activating = plugin.activate(context); 196 | var expected = { 197 | revisionData: { 198 | activatedRevisionKey: '89b1d82820a24bfb075c5b43b36f454b' 199 | } 200 | }; 201 | 202 | return assert.isFulfilled(activating).then(function(revisionData) { 203 | assert.deepEqual(expected, revisionData); 204 | }); 205 | }); 206 | 207 | }); 208 | 209 | describe('upload hook', function() { 210 | it('updates revisionManifest', function() { 211 | var manifestPath = "/usr/local/www/my-app/revisions.json"; 212 | var revisions = [{"revision": "4564564545646"}]; 213 | var client = plugin._client; 214 | var files = {}; 215 | files[manifestPath] = JSON.stringify(revisions); 216 | 217 | client._uploadedFiles = files; 218 | 219 | var uploading = plugin.upload(context); 220 | 221 | return assert.isFulfilled(uploading).then(function() { 222 | var manifest = client._uploadedFiles[manifestPath]; 223 | revisions.unshift({'revision': '89b1d82820a24bfb075c5b43b36f454b'}); 224 | assert.equal(JSON.stringify(revisions), manifest); 225 | }); 226 | }); 227 | 228 | it('uploads applicationFiles', function() { 229 | // var client = plugin._client; 230 | // var uploading = plugin.upload(context); 231 | 232 | 233 | // return assert.isFulfilled(uploading).then(function() { 234 | 235 | // var index = client._uploadedFiles['/usr/local/www/my-app/revisions/89b1d82820a24bfb075c5b43b36f454b/index.html'] 236 | 237 | // assert.equal(index, 'indexpage'); 238 | // }); 239 | }); 240 | }); 241 | 242 | }); -------------------------------------------------------------------------------- /tests/unit/lib/ssh-client-nodetest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('../../helpers/assert'); 4 | var Client = require('../../../lib/ssh-client'); 5 | 6 | describe('ssh-client', function() { 7 | var options = { 8 | username: 'aaron', 9 | privateKeyPath: null, 10 | host: "mydomain.com", 11 | agent: null, 12 | port: 22 13 | }; 14 | 15 | describe('#init', function() { 16 | 17 | it('sets options', function() { 18 | // var options = lodash.omit(options, 'username'); 19 | var client = new Client(options); 20 | 21 | assert.equal(client.options, options); 22 | }); 23 | 24 | }); 25 | }); 26 | --------------------------------------------------------------------------------