ssh-deploy-release
Deploy releases over SSH with rsync, archive ZIP / TAR, symlinks, SCP ...
Example :
/deployPath
|
├── www --> symlink to ./releases/<currentRelease>
|
├── releases
| ├── 2017-02-08-17-14-21-867-UTC
| ├── ...
| └── 2017-02-09-18-01-10-765-UTC
| ├── ...
| └── logs --> symlink to shared/logs
|
├── synchronized --> folder synchronized with rsync
|
└── shared
└── logs
Installation
npm install ssh-deploy-release
Usage
Deploy release
const Application = require('ssh-deploy-release');
const options = {
localPath: 'src',
host: 'my.server.com',
username: 'username',
password: 'password',
deployPath: '/var/www/vhost/path/to/project'
};
const deployer = new Application(options);
deployer.deployRelease(() => {
console.log('Ok !')
});
Remove release
const Application = require('ssh-deploy-release');
const options = {
localPath: 'src',
host: 'my.server.com',
username: 'username',
password: 'password',
deployPath: '/var/www/vhost/path/to/project',
allowRemove: true
};
const deployer = new Application(options);
deployer.removeRelease(() => {
console.log('Ok !')
});
Rollback to previous release
const Application = require('ssh-deploy-release');
const options = {
localPath: 'src',
host: 'my.server.com',
username: 'username',
password: 'password',
deployPath: '/var/www/vhost/path/to/project'
};
const deployer = new Application(options);
deployer.rollbackToPreviousRelease(() => {
console.log('Ok !')
});
The previous release will be renamed before updating the symlink of the current version, for example
2019-01-09-10-53-35-265-UTC
will become
2019-01-09-13-46-45-457-UTC_rollback-to_2019-01-09-10-53-35-265-UTC
.
If rollbackToPreviousRelease
is called several times, the current version
will switch between the last two releases.
current date + "_rollbackTo_"
will be prepended to the release name on each call of
rollbackToPreviousRelease
so be careful not to exceed the size limit of the folder name.
Use with Grunt
Platform support
You can use this to deploy from any platform supporting Node >= 8 to Linux servers (most UNIX systems should work as well but this hasn't been tested).
Due to how we implemented the deployment process on the remote environment (using shell command execution), supporting Windows would required a lot of specific code, which would make this package harder to maintain. We decided to focus on supporting Linux as its the platform most widely used by hosting providers.
Options
ssh-deploy-release uses ssh2 to handle SSH connections.
The options
object is forwarded to ssh2
methods,
which means you can set all ssh2
options:
options.debug
If true
, will display all commands.
Default : false
options.port
Port used to connect to the remote server.
Default : 22
options.host
Remote server hostname.
options.username
Username used to connect to the remote server.
options.password
Password used to connect to the remote server.
Default: null
options.privateKeyFile
Default: null
options.passphrase
For an encrypted private key, this is the passphrase used to decrypt it.
Default: null
options.agent
To connect using the machine's ssh-agent. The value must be the path to the ssh-agent socket (usually available in the
SSH_AUTH_SOCK
environment variable).
options.mode
archive
: Deploy an archive and decompress it on the remote server.
synchronize
: Use rsync. Files are synchronized in the options.synchronized
folder on the remote server.
Default : archive
options.archiveType
zip
: Use zip compression (unzip
command on remote)
tar
: Use tar gz compression (tar
command on remote)
Default : tar
options.archiveName
Name of the archive.
Default : release.tar.gz
options.deleteLocalArchiveAfterDeployment
Delete the local archive after the deployment.
Default : true
options.readyTimeout
SCP connection timeout duration.
Default : 20000
options.onKeyboardInteractive
Callback passed to ssh2
client event
keyboard-interactive
.
Type: function (name, descr, lang, prompts, finish)
Path
options.currentReleaseLink
Name of the current release symbolic link. Relative to deployPath
.
Defaut : www
options.sharedFolder
Name of the folder containing shared folders. Relative to deployPath
.
Default : shared
options.releasesFolder
Name of the folder containing releases. Relative to deployPath
.
Default : releases
options.localPath
Name of the local folder to deploy.
Default : www
localPath
to an empty string, null
or .
. Use process.cwd()
to have node generate an
absolute path. In addition to this, if you use the archive mode,
don't forget to exclude the generated archive
(you can define its name using options.archiveName).
Example:
const Application = require('ssh-deploy-release');
const process = require('process');
const deployer = new Application({
localPath: process.cwd(),
exclude: ['release.tar.gz'],
archiveName: 'release.tar.gz',
host: 'my.server.com',
username: 'username',
password: 'password',
deployPath: '/var/www/vhost/path/to/project',
});
options.deployPath
Absolute path on the remote server where releases will be deployed. Do not specify currentReleaseLink (or www folder) in this path.
options.synchronizedFolder
Name of the remote folder where rsync synchronize release.
Used when mode
is 'synchronize'.
Default : www
options.rsyncOptions
Additional options for rsync process.
Default : ''
rsyncOptions : '--exclude-from="exclude.txt" --delete-excluded'
options.compression
Enable the rsync --compression flag. This can be set to a boolean or an integer to explicitly set the compression level (--compress-level=NUM).
Default : true
Releases
options.releasesToKeep
Number of releases to keep on the remote server.
Default : 3
options.tag
Name of the release. Must be different for each release.
Default : Use current timestamp.
options.exclude
List of paths to not deploy.
Paths must be relative to localPath
.
The format slightly differ depending on the mode
:
-
glob format for
mode: 'archive'
In order to exclude a folder, you have to explicitly ignore all its descending files using**
.
For example:exclude: ['my-folder/**']
Read glob documentation for more information.
-
rsync exclude pattern format for
mode: 'synchronize'
In order to exclude a folder, you simply have to list it, all of its descendants will be excluded as well.
For example:exclude: ['my-folder']
For maximum portability, it's strongly advised to use both syntaxes when excluding folders.
For example: exclude: ['my-folder/**', 'my-folder']
Default : []
options.share
List of folders to "share" between releases. A symlink will be created for each item.
Item can be either a string or an object (to specify the mode to set to the symlink target).
share: {
'images': 'assets/images',
'upload': {
symlink: 'app/upload',
mode: '777' // Will chmod 777 shared/upload
}
}
Keys = Folder to share (relative to sharedFolder
)
Values = Symlink path (relative to release folder)
Default : {}
options.create
List of folders to create on the remote server.
Default : []
options.makeWritable
List of files to make writable on the remote server. (chmod ugo+w)
Default : []
options.makeExecutable
List of files to make executable on the remote server. (chmod ugo+x)
Default : []
options.allowRemove
If true, the remote release folder can be deleted with removeRelease
method.
Default: false
Callbacks
context object
The following object is passed to onXXX
callbacks :
{
// Loaded configuration
options: { },
// Release
release: {
// Current release name
tag: '2017-01-25-08-40-15-138-UTC',
// Current release path on the remote server
path: '/opt/.../releases/2017-01-25-08-40-15-138-UTC',
},
// Logger methods
logger: {
// Log fatal error and stop process
fatal: (message) => {},
// Log 'subhead' message
subhead: (message) => {},
// Log 'ok' message
ok: (message) => {},
// Log 'error' message and continue process
error: (message) => {},
// Log message, only if options.debug is true
debug: (message) => {},
// Log message
log: (message) => {},
// Start a spinner and display message
// return a stop()
startSpinner: (message) => { return {stop: () => {}}},
},
// Remote server methods
remote: {
// Excute command on the remote server
exec: (command, done, showLog) => {},
// Excute multiple commands (array) on the remote server
execMultiple: (commands, done, showLog) => {},
/*
* Upload local src file to target on the remote server.
* @param {string} src The path to the file to upload.
* May be either absolute or relative to the current working directory.
* @param {string} target The path of the uploaded file on the remote server.
* Must include the filename. The full directory hierarchy to the target must already exist.
* May be either absolute or relative to the remote user home directory.
* We strongly encourage you to use `options.deployPath` in your target path to produce an absolute path.
*/
upload: (src, target, done) => {},
// Create a symbolic link on the remote server
createSymboliclink: (target, link, done) => {},
// Chmod path on the remote server
chmod: (path, mode, done) => {},
// Create folder on the remote server
createFolder: (path, done) => {},
}
}
Examples
onBeforeDeploy, onBeforeLink, onAfterDeploy, onBeforeRollback, onAfterRollback options.
Single command executed on remote
onAfterDeploy: 'apachectl graceful'
Or with a function :
onBeforeLink: context => `chgrp -R www ${context.release.path}`
List of commands executed on remote
onAfterDeploy: [
'do something on the remote server',
'and another thing'
]
Or with a function :
onBeforeLink: (context) => {
context.logger.subhead('Fine tuning permissions on newly deployed release');
return [
`chgrp -R www ${context.release.path}`,
`chmod g+w ${context.release.path}/some/path/that/needs/to/be/writable/by/www/group`,
];
}
Custom callback
onAfterDeploy: context => {
return Promise((resolve, reject) => {
setTimeout(function () {
// Do something
resolve();
}, 5000);
});
}
options.onBeforeConnect
Executed before connecting to the SSH server to let you initiate a custom
connection. It must return a ssh2 Client instance, and call onReady
when that
connection is ready.
Type: function(context, onReady, onError, onClose): Client
Example: SSH jumps (connecting to your deployment server through a bastion)
onBeforeConnect: (context, onReady, onError, onClose) => {
const bastion = new Client();
const connection = new Client();
bastion.on('error', onError);
bastion.on('close', onClose);
bastion.on('ready', () => {
bastion.forwardOut(
'127.0.0.1',
12345,
'www.example.com',
22,
(err, stream) => {
if (err) {
context.logger.fatal(`Error connection to the bastion: ${err}`);
bastion.end();
onClose();
return;
}
connection.connect({
sock: stream,
user: 'www-user',
password: 'www-password',
});
}
);
});
connection.on('error', (err) => {
context.logger.error(err);
bastion.end();
});
connection.on('close', () => {
bastion.end();
});
connection.on('ready', onReady);
bastion.connect({
host: 'bastion.example.com',
user: 'bastion-user',
password: 'bastion-password',
});
return connection;
}
options.onBeforeDeploy
Executed before deployment.
Type: string | string[] | function(context, done): Promise | undefined
options.onBeforeLink
Executed before symlink creation.
Type: string | string[] | function(context, done): Promise | undefined
options.onAfterDeploy
Executed after deployment.
Type: string | string[] | function(context, done): Promise | undefined
options.onBeforeRollback
Executed before rollback to previous release.
Type: string | string[] | function(context, done): Promise | undefined
options.onAfterRollback
Executed after rollback to previous release.
Type: string | string[] | function(context, done): Promise | undefined
Known issues
Command not found or not executed
A command on a callback method is not executed or not found.
Try to add set -i && source ~/.bashrc &&
before your commmand :
onAfterDeploy:[
'set -i && source ~/.bashrc && my command'
]
See this issue : https://github.com/mscdex/ssh2/issues/77
Contributing
# Build (with Babel)
npm run build
# Build + watch (with Babel)
npm run build -- --watch
# Launch tests (Mocha + SinonJS)
npm test
# Launch tests + watch (Mocha + SinonJS)
npm test -- --watch