背景
测试环境的部署,一直以来都是登录服务器进行操作,一套登录验证下来很浪费时间。找了一个其他大佬开发的部署库,但是缺少使用文档,参数不明。想着逻辑不是很复杂,自己开发一套,还可以增加一些自定义功能,方便使用。
部署脚本步骤
- 压缩需要部署的文件
- 连接服务器
- 上传压缩文件
- 解压文件
所需插件
安装 文件压缩插件 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;
});
}