la-haute-societe / ssh-deploy-release

Deploy releases over SSH with rsync, archive ZIP / TAR, symlinks, SCP ...
https://www.npmjs.com/package/ssh-deploy-release
MIT License
36 stars 10 forks source link

ssh-deploy-release

NPM version npm (tag)

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

Use grunt-ssh-deploy-release.

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

⚠ ️In case you need to deploy your whole project directory, do NOT set 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:

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