├── test ├── fixtures │ ├── 123 │ └── testing ├── expected │ ├── custom_options │ └── default_options └── ssh_deploy_test.js ├── .gitignore ├── .jshintrc ├── secret.json ├── LICENSE-MIT ├── package.json ├── Gruntfile.js ├── tasks ├── ssh_rollback.js └── ssh_deploy.js └── README.md /test/fixtures/123: -------------------------------------------------------------------------------- 1 | 1 2 3 -------------------------------------------------------------------------------- /test/fixtures/testing: -------------------------------------------------------------------------------- 1 | Testing -------------------------------------------------------------------------------- /test/expected/custom_options: -------------------------------------------------------------------------------- 1 | Testing: 1 2 3 !!! -------------------------------------------------------------------------------- /test/expected/default_options: -------------------------------------------------------------------------------- 1 | Testing, 1 2 3. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true 13 | } 14 | -------------------------------------------------------------------------------- /secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "staging": { 3 | "host": "staging-host", 4 | "username": "", 5 | "password": "", 6 | "port": "22" 7 | }, 8 | "production": { 9 | "host": "production-host", 10 | "username": "", 11 | "password": "", 12 | "port": "22" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Dustin Carlson 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-ssh-deploy", 3 | "description": "Grunt SSH deployment", 4 | "version": "0.4.1", 5 | "homepage": "https://github.com/dasuchin/grunt-ssh-deploy", 6 | "author": { 7 | "name": "Dustin Carlson", 8 | "email": "dustin@dustincarlson.org", 9 | "url": "http://dustincarlson.org" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/dasuchin/grunt-ssh-deploy.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/dasuchin/grunt-ssh-deploy/issues" 17 | }, 18 | "licenses": [ 19 | { 20 | "type": "MIT", 21 | "url": "https://github.com/dasuchin/grunt-ssh-deploy/blob/master/LICENSE-MIT" 22 | } 23 | ], 24 | "engines": { 25 | "node": ">= 0.12.0" 26 | }, 27 | "scripts": { 28 | "test": "grunt test" 29 | }, 30 | "devDependencies": { 31 | "grunt-contrib-jshint": "^0.9.2", 32 | "grunt-contrib-clean": "^0.5.0", 33 | "grunt-contrib-nodeunit": "^0.3.3", 34 | "grunt": "~0.4.4" 35 | }, 36 | "peerDependencies": { 37 | "grunt": ">=0.4.0" 38 | }, 39 | "keywords": [ 40 | "gruntplugin", 41 | "ssh", 42 | "deploy", 43 | "releases" 44 | ], 45 | "dependencies": { 46 | "async": "^0.8.0", 47 | "extend": "^1.3.0", 48 | "moment": "^2.6.0", 49 | "ssh2": "^0.2.22", 50 | "scp2": "^0.1.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/ssh_deploy_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('grunt'); 4 | 5 | /* 6 | ======== A Handy Little Nodeunit Reference ======== 7 | https://github.com/caolan/nodeunit 8 | 9 | Test methods: 10 | test.expect(numAssertions) 11 | test.done() 12 | Test assertions: 13 | test.ok(value, [message]) 14 | test.equal(actual, expected, [message]) 15 | test.notEqual(actual, expected, [message]) 16 | test.deepEqual(actual, expected, [message]) 17 | test.notDeepEqual(actual, expected, [message]) 18 | test.strictEqual(actual, expected, [message]) 19 | test.notStrictEqual(actual, expected, [message]) 20 | test.throws(block, [error], [message]) 21 | test.doesNotThrow(block, [error], [message]) 22 | test.ifError(value) 23 | */ 24 | 25 | exports.ssh_deploy = { 26 | setUp: function(done) { 27 | // setup here if necessary 28 | done(); 29 | }, 30 | default_options: function(test) { 31 | test.expect(1); 32 | 33 | var actual = grunt.file.read('tmp/default_options'); 34 | var expected = grunt.file.read('test/expected/default_options'); 35 | test.equal(actual, expected, 'should describe what the default behavior is.'); 36 | 37 | test.done(); 38 | }, 39 | custom_options: function(test) { 40 | test.expect(1); 41 | 42 | var actual = grunt.file.read('tmp/custom_options'); 43 | var expected = grunt.file.read('test/expected/custom_options'); 44 | test.equal(actual, expected, 'should describe what the custom option(s) behavior is.'); 45 | 46 | test.done(); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-ssh-deploy 3 | * https://github.com/dcarlson/grunt-ssh-deploy 4 | * 5 | * Copyright (c) 2014 Dustin Carlson 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = function(grunt) { 12 | 13 | // Project configuration. 14 | grunt.initConfig({ 15 | jshint: { 16 | all: [ 17 | 'Gruntfile.js', 18 | 'tasks/*.js', 19 | '<%= nodeunit.tests %>' 20 | ], 21 | options: { 22 | jshintrc: '.jshintrc' 23 | } 24 | }, 25 | 26 | // Before generating any new files, remove any previously-created files. 27 | clean: { 28 | tests: ['tmp'] 29 | }, 30 | 31 | // Configuration to be run (and then tested). 32 | secret: grunt.file.readJSON('secret.json'), 33 | environments: { 34 | staging: { 35 | options: { 36 | host: '<%= secret.staging.host %>', 37 | username: '<%= secret.staging.username %>', 38 | password: '<%= secret.staging.password %>', 39 | port: '<%= secret.staging.port %>', 40 | deploy_path: '/full/path', 41 | local_path: 'dist', 42 | current_symlink: 'current', 43 | debug: true 44 | } 45 | }, 46 | production: { 47 | options: { 48 | host: '<%= secret.production.host %>', 49 | username: '<%= secret.production.username %>', 50 | password: '<%= secret.production.password %>', 51 | port: '<%= secret.production.port %>', 52 | deploy_path: '/full/path', 53 | local_path: 'dist', 54 | current_symlink: 'current' 55 | } 56 | } 57 | }, 58 | 59 | // Unit tests. 60 | nodeunit: { 61 | tests: ['test/*_test.js'] 62 | } 63 | 64 | }); 65 | 66 | // Actually load this plugin's task(s). 67 | grunt.loadTasks('tasks'); 68 | 69 | // These plugins provide necessary tasks. 70 | grunt.loadNpmTasks('grunt-contrib-jshint'); 71 | grunt.loadNpmTasks('grunt-contrib-clean'); 72 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 73 | 74 | // Whenever the "test" task is run, first clean the "tmp" dir, then run this 75 | // plugin's task(s), then test the result. 76 | grunt.registerTask('test', ['clean', 'ssh_deploy', 'ssh_rollback', 'nodeunit']); 77 | 78 | // By default, lint and run all tests. 79 | grunt.registerTask('default', ['jshint', 'test']); 80 | 81 | }; 82 | -------------------------------------------------------------------------------- /tasks/ssh_rollback.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-ssh-deploy 3 | * https://github.com/dcarlson/grunt-ssh-deploy 4 | * 5 | * Copyright (c) 2014 Dustin Carlson 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var path = require('path'); 12 | 13 | module.exports = function(grunt) { 14 | 15 | grunt.registerTask('ssh_rollback', 'Begin Rollback', function() { 16 | var done = this.async(); 17 | var Connection = require('ssh2'); 18 | var async = require('async'); 19 | var extend = require('extend'); 20 | 21 | var defaults = { 22 | current_symlink: 'current', 23 | port: 22, 24 | max_buffer: 400 * 1024, 25 | release_root: 'releases', 26 | release_subdir: '/' 27 | }; 28 | 29 | var options = extend({}, defaults, grunt.config.get('environments').options, 30 | grunt.config.get('environments')[this.args]['options']); 31 | 32 | var c = new Connection(); 33 | c.on('connect', function() { 34 | grunt.log.subhead('Connecting :: ' + options.host); 35 | }); 36 | c.on('ready', function() { 37 | grunt.log.subhead('Connected :: ' + options.host); 38 | // execution of tasks 39 | execCommands(options,c); 40 | }); 41 | c.on('error', function(err) { 42 | grunt.log.subhead("Error :: " + options.host); 43 | grunt.log.errorlns(err); 44 | if (err) {throw err;} 45 | }); 46 | c.on('close', function(had_error) { 47 | grunt.log.subhead("Closed :: " + options.host); 48 | 49 | return true; 50 | }); 51 | c.connect(options); 52 | 53 | var execCommands = function(options, connection){ 54 | 55 | // executes a remote command via ssh 56 | var execRemote = function(cmd, showLog, next){ 57 | connection.exec(cmd, function(err, stream) { 58 | if (err) { 59 | grunt.log.errorlns(err); 60 | grunt.log.subhead('ERROR ROLLING BACK. CLOSING CONNECTION.'); 61 | } 62 | stream.on('data', function(data, extended) { 63 | grunt.log.debug((extended === 'stderr' ? 'STDERR: ' : 'STDOUT: ') + data); 64 | }); 65 | stream.on('end', function() { 66 | grunt.log.debug('REMOTE: ' + cmd); 67 | if(!err) { 68 | next(); 69 | } 70 | }); 71 | }); 72 | }; 73 | 74 | var updateSymlink = function(callback) { 75 | var delete_symlink = 'rm -rf ' + path.posix.join(options.deploy_path, options.current_symlink); 76 | var set_symlink = 'cd ' + options.deploy_path + ' && t=`ls -t1 ' + path.posix.join(options.deploy_path, options.release_root, options.release_subdir) + ' | sed -n 2p` && ln -s ' + path.posix.join(options.deploy_path, options.release_root, options.release_subdir) + '$t ' + options.current_symlink; 77 | var command = delete_symlink + ' && ' + set_symlink; 78 | grunt.log.subhead('--------------- UPDATING SYM LINK'); 79 | grunt.log.subhead('--- ' + command); 80 | execRemote(command, options.debug, callback); 81 | }; 82 | 83 | var deleteRelease = function(callback) { 84 | var command = 't=`ls -t1 ' + path.posix.join(options.deploy_path, options.release_root, options.release_subdir) + ' | sed -n 1p` && rm -rf ' + path.posix.join(options.deploy_path, options.release_root, options.release_subdir) + '$t/'; 85 | grunt.log.subhead('--------------- DELETING RELEASE'); 86 | grunt.log.subhead('--- ' + command); 87 | execRemote(command, options.debug, callback); 88 | }; 89 | 90 | // closing connection to remote server 91 | var closeConnection = function(callback) { 92 | connection.end(); 93 | 94 | callback(); 95 | }; 96 | 97 | async.series([ 98 | updateSymlink, 99 | deleteRelease, 100 | closeConnection 101 | ], done); 102 | }; 103 | }); 104 | }; 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-ssh-deploy (Version: 0.4.1) 2 | 3 | > SSH Deployment for Grunt using [ssh2](https://github.com/mscdex/ssh2). 4 | 5 | ## Getting Started 6 | This plugin requires Grunt `~0.4.4` 7 | 8 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 9 | 10 | ```shell 11 | npm install grunt-ssh-deploy --save-dev 12 | ``` 13 | 14 | Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: 15 | 16 | ```js 17 | grunt.loadNpmTasks('grunt-ssh-deploy'); 18 | ``` 19 | 20 | ## The tasks 21 | 22 | ### Overview 23 | In your project's Gruntfile, add a section named `environments` to the data object passed into `grunt.initConfig()`. 24 | 25 | ```js 26 | grunt.initConfig({ 27 | environments: { 28 | environment: { 29 | // Environment specific options here 30 | } 31 | }, 32 | }); 33 | ``` 34 | 35 | This plugin will connect to your remote host, add a directory to releases/ in your `remote_path`, and create a symlink to the latest release. 36 | 37 | The symlink by default is `current`, you can change this by setting `current_symlink`. 38 | 39 | ### Usage 40 | By setting an environment, you can deploy each specific one with `grunt ssh_deploy:environment` or rolling back with `grunt ssh_rollback:environment`. 41 | 42 | ### Options 43 | 44 | #### options.host 45 | Type: `String` 46 | 47 | Remote host to connect to. 48 | 49 | #### options.username 50 | Type: `String` 51 | 52 | The username to connect as on the remote server. 53 | 54 | #### options.password 55 | Type: `String` 56 | 57 | Password for the username on the remote server. 58 | 59 | #### options.privateKey 60 | Type: `string` 61 | 62 | Path to your private key `privateKey: require('fs').readFileSync('/path/to/private/key')` 63 | 64 | #### options.passphrase 65 | Type: `string` 66 | 67 | Passphrase of your private key if needed. 68 | 69 | #### options.agent 70 | Type: `string` 71 | 72 | Set agent `agent: process.env.SSH_AUTH_SOCK` 73 | 74 | #### options.port 75 | Type: `String` 76 | Default value: `'22'` 77 | 78 | Port to connect to on the remote server. 79 | 80 | #### options.readyTimeout 81 | Type: `Number` 82 | Default value: `20000` 83 | 84 | Default timeout (in milliseconds) to wait for the SSH handshake to complete. 85 | 86 | #### options.deploy_path 87 | Type: `String` 88 | 89 | Full path on the remote server where files will be deployed. 90 | 91 | #### options.local_path 92 | Type: `String` 93 | 94 | Path on your local for the files you want to be deployed to the remote server. No trailing slash needed. 95 | 96 | #### options.current_symlink 97 | Type: `String` 98 | Default value: `'current'` 99 | 100 | Path to directory to symlink with most recent release. 101 | 102 | #### options.before_deploy, options.after_deploy 103 | Type: `String` 104 | 105 | Commands to run on the server before and after deploy directory is created and symlinked. 106 | 107 | #### options.tag 108 | Type: `String|function` 109 | Default value: the current date (as a timestamp) 110 | 111 | The release tag, e.g. '1.2.3'. It can be a string or a function (in that case is called and the returned value will be 112 | used). It defaults to the current timestamp formatted as 'YYYYMMDDHHmmssSSS'. 113 | 114 | *WARN*: release tag name **matters**. When used with parameter `releases_to_keep` the releases are reverse sorted 115 | alphabetically and older ones are removed. So be careful when you set your release tag name. 116 | 117 | #### options.releases_to_keep 118 | Type: `Number` 119 | 120 | The number of builds (including the current build) to keep in the remote releases directory. Must be >= 1. 121 | 122 | #### options.release_subdir 123 | Type: `String` 124 | Default value: `'/'` 125 | 126 | Name of the sub directory to store the release in. Useful when multiple projects get deployed 127 | to the same machine and the `releases_to_keep` option is being used. 128 | 129 | #### options.release_root 130 | Type: `String` 131 | Default value: `'releases'` 132 | 133 | Name of the root directory where all the releases are published. If a `options.release_subdir` is also provided then 134 | the latest will be appended after this path. 135 | 136 | #### options.zip_deploy 137 | Type: `Boolean` 138 | Default value: `false` 139 | 140 | Compress the build before uploading. 141 | 142 | #### options.max_buffer 143 | Type: `Number` 144 | Default value: `200 * 1024` 145 | 146 | Largest amount of data allowed on stdout or stderr. 147 | 148 | #### options.exclude 149 | Type: `Array` 150 | Default value: `[]` 151 | 152 | List of folders or files to exclude from build. 153 | 154 | ### Usage Examples 155 | 156 | #### Custom Options 157 | 158 | ```js 159 | grunt.initConfig({ 160 | // do not store credentials in the git repo, store them separately and read from a secret file 161 | secret: grunt.file.readJSON('secret.json'), 162 | environments: { 163 | options: { 164 | local_path: 'dist', 165 | current_symlink: 'current', 166 | deploy_path: '/full/path' 167 | }, 168 | staging: { 169 | options: { 170 | host: '<%= secret.staging.host %>', 171 | username: '<%= secret.staging.username %>', 172 | password: '<%= secret.staging.password %>', 173 | port: '<%= secret.staging.port %>', 174 | debug: true, 175 | releases_to_keep: '3' 176 | } 177 | }, 178 | production: { 179 | options: { 180 | host: '<%= secret.production.host %>', 181 | username: '<%= secret.production.username %>', 182 | password: '<%= secret.production.password %>', 183 | port: '<%= secret.production.port %>', 184 | releases_to_keep: '5', 185 | release_subdir: 'myapp' 186 | } 187 | } 188 | } 189 | }); 190 | ``` 191 | 192 | ### Before and After Hooks 193 | ```js 194 | grunt.initConfig({ 195 | environments: { 196 | options: { 197 | local_path: '.', 198 | }, 199 | production: { 200 | options: { 201 | host: '123.45.67.89', 202 | username: 'root', 203 | password: 'password', 204 | deploy_path: '/sites/great_project', 205 | before_deploy: 'cd /sites/great_project/releases/current && forever stopall', 206 | after_deploy: 'cd /sites/great_project/releases/current && npm install && forever start app.js' 207 | } 208 | } 209 | } 210 | }); 211 | ``` 212 | 213 | ## Release History 214 | * 2014/06/23 - v0.2.0 - Added rollback functionality. 215 | * 2014/06/19 - v0.1.7 - Fixed symlink method to cd into deploy_path before setting symlink. 216 | * 2014/05/04 - v0.1.5 - Changing symlink method to not use full path. 217 | * 2014/05/04 - v0.1.0 - Initial release. 218 | -------------------------------------------------------------------------------- /tasks/ssh_deploy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-ssh-deploy 3 | * https://github.com/dcarlson/grunt-ssh-deploy 4 | * 5 | * Copyright (c) 2014 Dustin Carlson 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var path = require('path'); 12 | 13 | var getScpOptions = function(options) { 14 | var scpOptions = { 15 | port: options.port, 16 | host: options.host, 17 | username: options.username, 18 | readyTimeout: options.readyTimeout 19 | }; 20 | 21 | if (options.privateKey) { 22 | scpOptions.privateKey = options.privateKey; 23 | if (options.passphrase) { 24 | scpOptions.passphrase = options.passphrase; 25 | } 26 | } 27 | else if (options.password) { 28 | scpOptions.password = options.password; 29 | } 30 | else if (options.agent) { 31 | scpOptions.agent = options.agent; 32 | } else { 33 | throw new Error('Agent, Password or private key required for secure copy.'); 34 | } 35 | 36 | return scpOptions; 37 | }; 38 | 39 | module.exports = function(grunt) { 40 | 41 | grunt.registerTask('ssh_deploy', 'Begin Deployment', function() { 42 | var done = this.async(); 43 | var Connection = require('ssh2'); 44 | var client = require('scp2'); 45 | var moment = require('moment'); 46 | var timestamp = moment().format('YYYYMMDDHHmmssSSS'); 47 | var async = require('async'); 48 | var childProcessExec = require('child_process').exec; 49 | var extend = require('extend'); 50 | 51 | var defaults = { 52 | current_symlink: 'current', 53 | port: 22, 54 | zip_deploy: false, 55 | max_buffer: 200 * 1024, 56 | readyTimeout: 20000, 57 | release_subdir: '/', 58 | release_root: 'releases', 59 | tag: timestamp, 60 | exclude: [] 61 | }; 62 | 63 | var options = extend({}, defaults, grunt.config.get('environments').options, 64 | grunt.config.get('environments')[this.args]['options']); 65 | 66 | var releaseTag = typeof options.tag == 'function' ? options.tag() : options.tag; 67 | // Just a security check, avoiding empty tags that could mess up the file system 68 | if (releaseTag == '') { 69 | releaseTag = defaults.tag; 70 | } 71 | 72 | var releasePath = path.posix.join(options.deploy_path, options.release_root, options.release_subdir, releaseTag); 73 | 74 | // scp defaults 75 | client.defaults(getScpOptions(options)); 76 | 77 | var c = new Connection(); 78 | c.on('connect', function() { 79 | grunt.log.subhead('Connecting :: ' + options.host); 80 | }); 81 | c.on('ready', function() { 82 | grunt.log.subhead('Connected :: ' + options.host); 83 | // execution of tasks 84 | execCommands(options,c); 85 | }); 86 | c.on('error', function(err) { 87 | grunt.log.subhead("Error :: " + options.host); 88 | grunt.log.errorlns(err); 89 | if (err) {throw err;} 90 | }); 91 | c.on('close', function(had_error) { 92 | grunt.log.subhead("Closed :: " + options.host); 93 | 94 | return true; 95 | }); 96 | c.connect(options); 97 | 98 | var execCommands = function(options, connection){ 99 | var execLocal = function(cmd, next) { 100 | var execOptions = { 101 | maxBuffer: options.max_buffer 102 | }; 103 | 104 | childProcessExec(cmd, execOptions, function(err, stdout, stderr){ 105 | grunt.log.debug(cmd); 106 | grunt.log.debug('stdout: ' + stdout); 107 | grunt.log.debug('stderr: ' + stderr); 108 | if (err !== null) { 109 | grunt.log.errorlns('exec error: ' + err); 110 | grunt.log.subhead('Error deploying. Closing connection.'); 111 | 112 | deleteRelease(closeConnection); 113 | } else { 114 | next(); 115 | } 116 | }); 117 | }; 118 | 119 | // executes a remote command via ssh 120 | var execRemote = function(cmd, showLog, next){ 121 | connection.exec(cmd, function(err, stream) { 122 | if (err) { 123 | grunt.log.errorlns(err); 124 | grunt.log.subhead('ERROR DEPLOYING. CLOSING CONNECTION AND DELETING RELEASE.'); 125 | 126 | deleteRelease(closeConnection); 127 | } 128 | stream.on('data', function(data, extended) { 129 | grunt.log.debug((extended === 'stderr' ? 'STDERR: ' : 'STDOUT: ') + data); 130 | }); 131 | stream.on('end', function() { 132 | grunt.log.debug('REMOTE: ' + cmd); 133 | if(!err) { 134 | next(); 135 | } 136 | }); 137 | }); 138 | }; 139 | 140 | var zipForDeploy = function(callback) { 141 | if (!options.zip_deploy) return callback(); 142 | 143 | childProcessExec('tar --version', function (error, stdout, stderr) { 144 | if (!error) { 145 | var isGnuTar = stdout.match(/GNU tar/); 146 | var command = "tar -czvf ./deploy.tgz"; 147 | 148 | if(options.exclude.length) { 149 | options.exclude.forEach(function(exclusion) { 150 | command += ' --exclude=' + exclusion; 151 | }); 152 | } 153 | 154 | if (isGnuTar) { 155 | command += " --exclude=deploy.tgz --ignore-failed-read --directory=" + options.local_path + " ."; 156 | } else { 157 | command += " --directory=" + options.local_path + " ."; 158 | } 159 | 160 | grunt.log.subhead('--------------- ZIPPING FOLDER'); 161 | grunt.log.subhead('--- ' + command); 162 | 163 | execLocal(command, callback); 164 | } 165 | }); 166 | }; 167 | 168 | var onBeforeDeploy = function(callback){ 169 | if (typeof options.before_deploy === "undefined") return callback(); 170 | var command = options.before_deploy; 171 | grunt.log.subhead("--------------- RUNNING PRE-DEPLOY COMMANDS"); 172 | if (command instanceof Array) { 173 | async.eachSeries(command, function(command, callback) { 174 | grunt.log.subhead('--- ' + command); 175 | execRemote(command, options.debug, callback); 176 | }, callback); 177 | } else { 178 | grunt.log.subhead('--- ' + command); 179 | execRemote(command, options.debug, callback); 180 | } 181 | }; 182 | 183 | var createReleases = function(callback) { 184 | var command = 'mkdir -p ' + releasePath; 185 | grunt.log.subhead('--------------- CREATING NEW RELEASE'); 186 | grunt.log.subhead('--- ' + command); 187 | execRemote(command, options.debug, callback); 188 | }; 189 | 190 | var scpBuild = function(callback) { 191 | var build = (options.zip_deploy) ? 'deploy.tgz' : options.local_path; 192 | grunt.log.subhead('--------------- UPLOADING NEW BUILD'); 193 | grunt.log.debug('SCP FROM LOCAL: ' + build 194 | + '\n TO REMOTE: ' + releasePath); 195 | client.scp(build, { 196 | path: releasePath 197 | }, function (err) { 198 | if (err) { 199 | grunt.log.errorlns(err); 200 | } else { 201 | grunt.log.subhead('--- DONE UPLOADING'); 202 | callback(); 203 | } 204 | }); 205 | }; 206 | 207 | var unzipOnRemote = function(callback) { 208 | if (!options.zip_deploy) return callback(); 209 | var goToCurrent = "cd " + releasePath; 210 | var untar = "tar -xzvf deploy.tgz"; 211 | var cleanup = "rm " + path.posix.join(releasePath, "deploy.tgz"); 212 | var command = goToCurrent + " && " + untar + " && " + cleanup; 213 | grunt.log.subhead('--------------- UNZIP ZIPFILE'); 214 | grunt.log.subhead('--- ' + command); 215 | execRemote(command, options.debug, callback); 216 | }; 217 | 218 | var updateSymlink = function(callback) { 219 | var delete_symlink = 'rm -rf ' + path.posix.join(options.deploy_path, options.current_symlink); 220 | var set_symlink = 'ln -s ' + releasePath + ' ' + path.posix.join(options.deploy_path, options.current_symlink); 221 | var command = delete_symlink + ' && ' + set_symlink; 222 | grunt.log.subhead('--------------- UPDATING SYM LINK'); 223 | grunt.log.subhead('--- ' + command); 224 | execRemote(command, options.debug, callback); 225 | }; 226 | 227 | var deleteRelease = function(callback) { 228 | var command = 'rm -rf ' + releasePath; 229 | grunt.log.subhead('--------------- DELETING RELEASE'); 230 | grunt.log.subhead('--- ' + command); 231 | execRemote(command, options.debug, callback); 232 | }; 233 | 234 | var onAfterDeploy = function(callback){ 235 | if (typeof options.after_deploy === "undefined") return callback(); 236 | var command = options.after_deploy; 237 | grunt.log.subhead("--------------- RUNNING POST-DEPLOY COMMANDS"); 238 | if (command instanceof Array) { 239 | async.eachSeries(command, function(command, callback) { 240 | grunt.log.subhead('--- ' + command); 241 | execRemote(command, options.debug, callback); 242 | }, callback); 243 | } else { 244 | grunt.log.subhead('--- ' + command); 245 | execRemote(command, options.debug, callback); 246 | } 247 | }; 248 | 249 | var remoteCleanup = function(callback) { 250 | if (typeof options.releases_to_keep === 'undefined') return callback(); 251 | if (options.releases_to_keep < 1) options.releases_to_keep = 1; 252 | 253 | var command = "cd " + path.posix.join(options.deploy_path, options.release_root, options.release_subdir) + " && rm -rfv `ls -r " + path.posix.join(options.deploy_path, options.release_root, options.release_subdir) + " | awk 'NR>" + options.releases_to_keep + "'`"; 254 | grunt.log.subhead('--------------- REMOVING OLD BUILDS'); 255 | grunt.log.subhead('--- ' + command); 256 | execRemote(command, options.debug, callback); 257 | }; 258 | 259 | var deleteZip = function(callback) { 260 | if (!options.zip_deploy) return callback(); 261 | var command = 'rm deploy.tgz'; 262 | grunt.log.subhead('--------------- LOCAL CLEANUP'); 263 | grunt.log.subhead('--- ' + command); 264 | execLocal(command, callback); 265 | }; 266 | 267 | // closing connection to remote server 268 | var closeConnection = function(callback) { 269 | connection.end(); 270 | 271 | client.close(); 272 | client.__sftp = null; 273 | client.__ssh = null; 274 | 275 | callback(); 276 | }; 277 | 278 | async.series([ 279 | onBeforeDeploy, 280 | zipForDeploy, 281 | createReleases, 282 | scpBuild, 283 | unzipOnRemote, 284 | updateSymlink, 285 | onAfterDeploy, 286 | remoteCleanup, 287 | deleteZip, 288 | closeConnection 289 | ], function () { 290 | done(); 291 | }); 292 | }; 293 | }); 294 | }; 295 | --------------------------------------------------------------------------------