NodeJs Deploy Script for Small Projects with Yarn Workspaces

In preparation for my finals project, to have the implementation go more smoothly, I created an old-school Node.js script for uploading the applications to my test/demo server, which is an Ubuntu VPS running Caddy that handles the access to either the API or the Angular app (serving the files). Both are placed into directories inside /var/www/

The script can be run with yarn deploy [app | api] which reads the dist directory of the selected workspace from its package.json, then packages the code and uploads it to the server. If api is selected, it will also install dependencies and reload the application (using PM2).

Yes, PM2 already offers a script which achieves a similar result, but it cant' be set to copy a specific directory as it pulls the whole code from a remote repo. Hence I needed something else for my monorepo. Additionally I wanted to upload the code directly from the local machine and more control of what is happening.

Usage

  • Make sure the remote can be connected to, usually with an SSH config entry
  • Create a .env in the root of the project with the following variables:

REMOTE_ADDRESS - server where the app will be deployed

REMOTE_USER - user on the server which will execute the process, should have sufficient permissions

APP_REMOTE_DIRECTORY - where the app should be placed on the server, e.g. /var/www/app

API_REMOTE_DIRECTORY - where the api should be placed on the server

  • Specify a distDirectory in the package.json of each workspace
  • Install dotenv and tar
yarn add -D dotenv tar
  • Create the deployer.js in the root of the project:
require('dotenv').config();
const tar = require('tar');
const execSync = require('child_process').execSync;
const fs = require('fs');
const path = require('path');
const workspaces = require('./package.json').workspaces;

const REMOTE_ADDRESS = process.env.REMOTE_ADDRESS;
const REMOTE_USER = process.env.REMOTE_USER;
const APP_REMOTE_DIRECTORY = process.env.APP_REMOTE_DIRECTORY;
const API_REMOTE_DIRECTORY = process.env.API_REMOTE_DIRECTORY;

const target = process.argv.slice(2)[0];
const distDirs = [];

async function deploy() {
    //Check if selected workspace exists
    if (!workspaces.includes(target)) {
        console.log(`Invalid argument: The selected workspace does not exist!`);
        process.exit();
    }

    //Get each workspace's dist directory specified in their package.json
    workspaces.forEach(workspace => {
        const distDir = path.join(
            workspace,
            JSON.parse(fs.readFileSync(path.join(workspace, 'package.json'))).distDirectory
        );
        distDirs[workspace] = distDir;
    });

    //Package app into tarball
    if (!fs.existsSync('./tmp'))
        fs.mkdirSync('./tmp');
    await tar.create(
        {
            file: './tmp/package.tar',
            cwd: distDirs[target]
        },
        [ '.' ] //File selection inside the specified working (dist) directory
    ).catch((e) => {
        console.log(`Failed to package ${target}! ${e}`);
        process.exit();
    });

    //Copy package to server
    try {
        execSync(`scp ./tmp/package.tar ` +
            `${REMOTE_USER}@${REMOTE_ADDRESS}` +
            `:/tmp`,
            { stdio: [0, 1, 2] }
        );
    } catch (e) {
        console.log(`Failed to upload package to remote! ${e}`);
        process.exit();
    }

    //Extract into corresponding directory
    try {
        execSync(`ssh ${REMOTE_USER}@${REMOTE_ADDRESS} ` +
            `"cd /tmp; tar -xf package.tar ` +
            `-C ${target === 'app' ? APP_REMOTE_DIRECTORY : API_REMOTE_DIRECTORY} ` +
            `--no-overwrite-dir"`,
            { stdio: [0, 1, 2] }
        );
        console.log('Package extracted into destination');
    } catch (e) {
        console.log(`Failed to extract package! ${e}`);
        process.exit();
    }

    //Install dependencies and reload the application if needed
    if (target === 'api') {
        try {
            execSync(`ssh ${REMOTE_USER}@${REMOTE_ADDRESS} ` +
                `"cd ${API_REMOTE_DIRECTORY}; ` +
                `yarn install && pm2 startOrRestart ecosystem.config.js --env production"`,
                { stdio: [0, 1, 2] }
            );
            console.log('Dependencies installed and application reloaded');
        } catch (e) {
            console.log(`Failed to install dependencies or reload the application! ${e}`);
            process.exit();
        }
    }

    console.log('Application successfully deployed!');

    //Delete tmp
    try {
        fs.rmSync('./tmp', { recursive: true, force: true });
    } catch (e) {
        console.warn(`Failed to delete temporary package directory! ${e}`);
    }
}

module.exports = deploy();


  • Add as script to the root package.json
  "scripts": {
    "deploy": "node deployer"
  },


  • Run with yarn deploy [workspace]


Enjoy!




You'll only receive email when they publish something new.

More from Cili's Notes
All posts