前端自动化部署脚本

63 阅读3分钟

背景

测试环境的部署,一直以来都是登录服务器进行操作,一套登录验证下来很浪费时间。找了一个其他大佬开发的部署库,但是缺少使用文档,参数不明。想着逻辑不是很复杂,自己开发一套,还可以增加一些自定义功能,方便使用。

部署脚本步骤

  1. 压缩需要部署的文件
  2. 连接服务器
  3. 上传压缩文件
  4. 解压文件

所需插件

安装 文件压缩插件 archiver、服务器连接插件node-ssh、命令行插件 commander 、命令行交互插件 inquirer、控制台字符样式插件 chalk

实现

#!/usr/bin/env node
const path = require("path");
const archiver = require("archiver");
const fs = require("fs");
const inquirer = require("inquirer");
const { NodeSSH } = require("node-ssh");
const { log, err } = require("./utils");
const ssh = new NodeSSH();
const cwd = process.cwd();

let deployConfig = {};
let env = "";
let config = {};
let localPath = "";
let localZipPath = ""; // 压缩名件名
let remotePath = "";
let remoteZipPath = "";

run();

async function run() {
  try {
    deployConfig = await getConf();
    env = await getEnv();
    config = deployConfig[env];
    const paths = getPath();
    localPath = paths.localPath;
    localZipPath = paths.localZipPath;
    remotePath = paths.remotePath;
    remoteZipPath = paths.remoteZipPath;
    if (config.onlyExec) {
      doExec();
    } else {
      doJob();
    }
  } catch (error) {
    err(error);
  }
}

// 执行远端命令
async function doExec() {
  try {
    await connectSSH(); // 连接 ssh
    await runExec(config.onlyExec); // 连接 ssh
  } catch (error) {
    err(error);
  } finally {
    closeSSH();
    process.exit(0);
  }
}
//执行远端部署脚本
async function doJob() {
  try {
    await startZip(); // 压缩
    await connectSSH(); // 连接 ssh
    await uploadFile(); // 上传
    await removeRemoteFile(); // 移除原来的目录
    await unzipRemoteFile(); // 解压文件
    if (config.isRemoveRemoteZip) {
      await removeRemoteZip(); // 移除上传的压缩包
    }
    if (config.afterDoneExec) {
      await runExec(config.afterDoneExec); // 最后要在服务器执行shell
    }
    if (config.isRemoveLocalZip) {
      await removeLocalZip(); // 移除本地的压缩包
    }
  } catch (error) {
    err(error);
  } finally {
    closeSSH();
    process.exit(0);
  }
}

function getConf() {
  return new Promise((resolve, reject) => {
    const deployConfigPath = path.resolve(cwd, "./deploy.config.js");
    if (!fs.existsSync(deployConfigPath)) {
      err(`请添加deploy.config.js文件`, deployConfigPath);
      reject();
    }
    const deployConfig = require(deployConfigPath);
    if (
      !deployConfig ||
      !Object.prototype.toString.call(deployConfig) === "[object Object]" ||
      !Object.keys(deployConfig).length > 0
    ) {
      err(`需要完善配置信息`);
      reject();
    } else {
      resolve(deployConfig);
    }
  });
}

function getEnv() {
  let env = process.argv[2];
  if (env) {
    return env;
  } else {
    const envs = Object.keys(deployConfig);
    return inquirer
      .prompt([
        {
          name: "env",
          type: "list",
          message: `请选择发布的环境`,
          choices: envs.map((env) => {
            return { name: env, value: env };
          }),
          default: envs[0],
        },
      ])
      .then((answers) => {
        return answers["env"];
      })
      .catch((error) => {
        err("发布环境选择失败:", error);
        throw error;
      });
  }
}

function getPath() {
  const localPath = path.resolve(cwd, config.localDir); // 要上传的文件
  if (!fs.existsSync(localPath)) {
    err(`不存在${config.localDir}目录`);
    return;
  }
  const remoteDir = config.remoteDir || config.localDir;
  const localZipPath = path.resolve(cwd, `${remoteDir}.zip`); // 压缩名件名
  const remotePath = config.remotePath + `/${remoteDir}`;
  const remoteZipPath = config.remotePath + `/${remoteDir}.zip`;
  return {
    localPath: localPath,
    localZipPath: localZipPath,
    remotePath: remotePath,
    remoteZipPath: remoteZipPath,
  };
}

//压缩文件
function startZip() {
  log("开始压缩文件...");
  return new Promise((resolve, reject) => {
    const archive = archiver("zip", {
      zlib: { level: 9 }, //递归扫描最多5层
    }).on("error", function (error) {
      err("压缩文件失败:", error);
      reject(error); //压缩过程中如果有错误则抛出
    });
    const output = fs
      .createWriteStream(localZipPath)
      .on("close", function (error) {
        /*压缩结束时会触发close事件,然后才能开始上传,
          否则会上传一个内容不全且无法使用的zip包*/
        if (error) {
          err("关闭archiver异常:", error);
          reject(error);
          return;
        }
        log("压缩文件成功");
        resolve();
      });

    archive.pipe(output); // 压缩内容输出到 zip
    archive.directory(localPath, false); // 压缩内容直接放在zip包的根目录中
    archive.finalize(); // 执行打包
  });
}
// 连接服务器
function connectSSH() {
  log("开始ssh连接...");
  return ssh
    .connect({
      host: config.host,
      port: config.port,
      username: config.username,
      password: config.password,
    })
    .then(function () {
      log("ssh连接成功");
    })
    .catch((error) => {
      err("ssh连接失败:", error);
      throw error;
    });
}
function closeSSH() {
  if (ssh) {
    ssh.dispose();
  }
}
// 上传文件
function uploadFile() {
  log("上传本地压缩文件:", localZipPath, "到", remoteZipPath);
  //上传网站的发布包至configs中配置的远程服务器的指定地址
  return ssh
    .putFile(localZipPath, remoteZipPath)
    .then(function (status) {
      log("上传文件成功");
    })
    .catch((error) => {
      err("上传文件失败:", error);
      throw error;
    });
}
// 解压上传的文件
function unzipRemoteFile() {
  log("开始解压文件:", remoteZipPath, "到", remotePath);
  return ssh
    .execCommand(`unzip -o ${remoteZipPath} -d ${remotePath}`)
    .then(() => {
      log("解压成功");
    })
    .catch((error) => {
      err("解压失败");
      throw error;
    });
}
function removeRemoteFile() {
  log("删除远程文件:" + remotePath);
  return ssh
    .execCommand(`rm -rf ${remotePath}`)
    .then(() => {
      log("删除远程文件成功");
    })
    .catch((error) => {
      err("删除远程文件失败");
      throw error;
    });
}
function removeRemoteZip() {
  log("删除远程压缩文件:" + remoteZipPath);
  return ssh
    .execCommand(`rm -rf ${remoteZipPath}`)
    .then(() => {
      log("删除远程压缩文件成功");
    })
    .catch((error) => {
      err("删除远程压缩文件失败");
      throw error;
    });
}
function removeLocalZip() {
  log("删除本地压缩文件:" + localZipPath);
  fs.unlinkSync(localZipPath);
  log("删除本地压缩文件成功");
}
function runExec(code) {
  return ssh
    .execCommand(code)
    .then((result) => {
      console.log("远程STDOUT输出: " + result.stdout);
      if (!result.stderr) {
        log("执行命令成功", code);
      }
    })
    .catch((error) => {
      log("执行命令成功");
      throw error;
    });
}