qspark-deploy
Version:
齐圣园区自动化部署
413 lines (371 loc) • 12 kB
JavaScript
const path = require('path')
const moment = require('moment')
const util = require('util')
const events = require('events')
const Client = require('ssh2').Client
const fs = require('fs')
const ProgressBar = require('progress')
const simpleGit = require('simple-git')
const inquirer =require('inquirer')
const gitInfo = require("./git_info");
const axios = require("axios");
const archiver = require('archiver');
//cnpm install moment util events ssh2 progress inquirer@^8.0.0 simple-git axios --dev 请先在deploy所在目录安装以上包
/******************************请手动配置以下内容*********************************/
/** 远程服务器配置
* @type {{password: string, port: number, host: string, username: string}}
*/
const server = {
host: '', //主机ip
port: 22, //SSH 连接端口
username: '', //用户名
password: '', //用户登录密码
}
let basePath = '/opt/project/***/html' //服务器网站根目录
const bakDirName = '.bak' + moment(new Date()).format('YYYY-M-D-HH:mm:ss') //备份文件名
let buildPath = path.resolve('./dist') //本地项目编译后的文件目录
const maxBakNumber = 2 //最多备份数
const serverPath = 'https://deploy-api.joubn.com'
// const serverPath = 'http://127.0.0.1:3005'
/**********************************配置结束***************************************/
let currentBranch = ''
let access_token
let repositoryName = ''
let isTest
let git
const dirArr = buildPath.split(path.sep)
const activeProjectName = dirArr[dirArr.length-2]
const tarName = `${activeProjectName}.tar`
const buildTARPath = path.join(buildPath,tarName)
if(!fs.existsSync(buildPath)){
console.log('\x1b[91m','项目未打包!\033[0m');
}else{
git = simpleGit(buildPath);
compressedTar()
}
function compressedTar(){
fs.existsSync(buildTARPath)?fs.unlinkSync(buildTARPath):'';
const extnames = ['.zip', '.rar', '.7z']; // 定义压缩文件扩展名数组
fs.readdir(buildPath, function (err, files) { // 读取文件夹中的所有文件
if (err) {
console.error(err);
return;
}
const compressedFiles = files.filter((file) => { // 过滤出压缩文件
const extname = path.extname(file).toLowerCase(); // 获取文件扩展名并转为小写
return extnames.includes(extname); // 判断该扩展名是否在压缩文件扩展名数组中
});
if(!compressedFiles.length){
getTable()
}else{
console.log('\x1b[91m','打包文件夹里包含压缩文件!\033[0m');
}
});
}
const getTrack = async (answers) => {
const status = await git.status();
const gitInfoScope = gitInfo()
const repositoryArr = gitInfoScope[`remote "origin"`].url.split('/')
repositoryName = repositoryArr[repositoryArr.length-1].split('.git')[0]
currentBranch = status.current
isTest = getProcessArg('test')?1:0
// console.log(currentBranch) //分支名称
// console.log(activeProjectName);
// console.log(repositoryName);
let activeServes = []
let allServes = []
const res = await axios.get(`${serverPath}/serverTable`,{params:{
password:answers.dir,
activeProjectName:activeProjectName,
currentBranch:currentBranch,
repositoryName:repositoryName,
isTest:isTest
}})
if(res.data.code==1){
access_token = res.data.data.access_token
activeServes = res.data.data.activeServes
allServes = res.data.data.allServes
} else {
console.log(res.data.message);
return
}
chooseChannel(allServes,activeServes,(chooses)=>{
const chooseServes = allServes.filter(item=>chooses.includes(item.name))
console.log('\033[40;32m 当前部署的是 〖'+ chooses.join('〗、〖') +'〗 的 '+ chooseServes[0].systemName +'系统\033[0m');
inquirer.prompt([{
type: 'confirm',
name: 'choice',
message: '确定部署:',
default: true,
}]).then(answers => {
if(answers.choice){
chooseServes.forEach(async item=>{
const fn = await init(item)
})
}
}).catch(error => {
console.log(error)
});
})
}
function getProcessArg(arg){
const args = process.argv.slice(2,process.argv.length)
return args.filter(item=>item.indexOf(arg)>-1)[0]
}
function getTable(){
const promptList = [
{
type: 'password',
message: '输入密码',
name: 'dir',
choices:['确定','取消'],
}
];
inquirer.prompt(promptList).then(answers => {
getTrack(answers)
}).catch(err=>{
console.log(err)
})
}
function chooseChannel(allServes,activeServes,callback){
if(activeServes.length){ // 选中当前项目(并放在列表第一位)
for (let index = 0; index < allServes.length; index++) {
const item = allServes[index];
if(item.name === activeServes[0].name){
item.checked = true
allServes.splice(index,1)
allServes.unshift(item);
break
}
}
}
if(allServes.length==0){
console.log('\x1b[91m','未配置服务器表!\033[0m');
return
}
inquirer
.prompt([{
type: 'checkbox',
name: 'choice',
message: '选择部署渠道:',
choices: allServes
}])
.then(answers => {
if(!answers.choice.length){
console.log('\x1b[91m','未选择服务器表!\033[0m');
return
}
callback(answers.choice)
})
.catch(err=>{
console.log(err)
});
}
function toTar(cb){
const output = fs.createWriteStream(tarName);
const archive = archiver('tar', {
zlib: { level: 9 } // 设置压缩级别
});
archive.on('error', function(err) {
throw err;
});
archive.on('finish', function() {
console.log('压缩完成: '+ archive.pointer() + ' total bytes');
fs.renameSync(tarName, path.join(buildPath,tarName));
cb()
});
// 将输出流绑定到archiver
archive.pipe(output);
archive.directory(buildPath, false);
// 结束归档过程
archive.finalize();
}
function doBakDir(server, basePath, callback){
getFileOrDirList(server, basePath, (err,list)=>{
if(err){
console.log(err)
return false
}
let obj = {}
const dir = fs.readdirSync(buildPath)
if(list.length){
dir.forEach(item=>{
doShell(server, `mv ${basePath}/${item} ${basePath}/${item+bakDirName}\nexit\n`) //备份远程目录文件
obj[item] = []
list.forEach(dir=>{
if(dir.indexOf(item+'.bak')>-1){
obj[item].push(dir)
}
})
obj[item].sort()
if(obj[item].length >= maxBakNumber){
doShell(server, `rm ${item.indexOf('.html')>-1?'-f':'-rf'} ${basePath}/${obj[item][0]} \nexit\n`) //删除多余备份
}
})
}
toTar(callback)
})
}
/**
* 描述:获取远程文件路径下文件列表信息
* 参数:server 远程电脑凭证;
* basePath 远程路径;
* then 回调函数
* 回调:then(err, dirs) : dir, 获取的列表信息
*/
function getFileOrDirList(server, basePath, then){
var cmd = "find " + basePath + " -maxdepth 1 "+ "\r\nexit\r\n";
doShell(server, cmd, function(err, data){
var arr = [];
var remoteFile = [];
arr = data.split("\r\n");
arr.forEach(function(dir){
if(dir.indexOf(basePath+'/') ==0){
remoteFile.push(dir);
}
});
remoteFile=remoteFile.map(item=>item.split('/')[item.split('/').length-1]).filter(item=> item&&item.trim()) //只保留第一层目录
remoteFile=Array.from(new Set(remoteFile)) //去重
then(err, remoteFile);
})
};
function doConnect(server, then) {
const conn = new Client()
conn.on('ready', function () {
then && then(conn)
}).on('error', function (err) {
console.error('connect error!', err)
}).on('close', function () {
conn.end()
}).connect(server)
}
function doShell(server, cmd, then) {
doConnect(server, function (conn) {
conn.shell(function (err, stream) {
if (err) throw err
else {
let buf = ''
stream.on('close', function () {
conn.end()
then && then(err, buf)
}).on('data', function (data) {
buf = buf + data
}).stderr.on('data', function (data) {
console.log('stderr: ' + data)
})
stream.end(cmd)
}
})
})
}
function Control() {
events.EventEmitter.call(this)
}
util.inherits(Control, events.EventEmitter)
const control = new Control()
control.on('doNext', function (todos, then) {
if (todos.length > 0) {
const func = todos.shift()
func(function (err, result) {
if (err) {
then(err)
throw err
} else {
control.emit('doNext', todos, then)
}
})
} else {
then(null)
}
})
function doUploadFile(server, localPath, to, remoteDir, then) {
doConnect(server, function (conn) {
conn.sftp(function (err, sftp) {
if (err) {
then(err)
} else {
sftp.fastPut(localPath, to, function (err, result) {
conn.end()
doShell(server, `tar -xvf '${to}' -C ${remoteDir} && rm -f '${to}' \nexit\n`)
then(err, result)
})
}
})
})
}
function doUploadDir(server, localDir, remoteDir, then) {
let files = [path.join(buildPath,tarName)]
// 创建远程目录
let todoDir = []
todoDir.push(function (done) {
const cmd = 'mkdir -p ' + remoteDir + '\r\nexit\r\n'
// console.log(`cmd::${cmd}`)
doShell(server, cmd, done)
}) // end of push
// 上传文件
console.log('准备上传文件:')
let todoFile = []
let total = files.length;
let bar = new ProgressBar('上传进度:[:bar] :percent 剩余时长::etas', {
total,
width: 50
});
files.forEach(function (file) {
todoFile.push(function (done) {
const to = path.join(remoteDir, file.slice(localDir.length + 1)).replace(/[\\]/g, '/')
doUploadFile(server, file, to,remoteDir, ()=>{bar.tick(1);done()})
})
})
control.emit('doNext', todoDir, function (err) {
if (err) {
throw err
} else {
control.emit('doNext', todoFile, then)
}
})
}
function sendRobot(item){
if(!access_token){
console.log('\x1b[91m','未配置钉钉事件消息token!\033[0m');
}
const gitinfo = gitInfo('global')
item.access_token = access_token
item.userName = gitinfo&&gitinfo.user&&gitinfo.user.name
item.currentBranch = currentBranch;
item.repositoryName = repositoryName;
item.isTest = isTest
axios.post(`${serverPath}/sendRobot`, item).then(res=>{
if(res.data.code!=1){
console.log(res.data.message);
}else{
console.log(res.data.message);
}
})
}
function init(item) {
if(item.distDic){ //自定义项目打包路径
buildPath = path.resolve(item.distDic)
}
return new Promise((resolve, reject)=>{
server.host = item.remoteHost
server.port = item.remotePort||22
server.username = item.remoteUsername
server.password = item.remotePassword
basePath = item.remoteDic
const serverCopy = JSON.parse(JSON.stringify(server))
const basePathCopy = basePath
console.log('\n--------配置如下--------------\n')
console.log(`服务器host: ${server.host}`)
console.log(`项目部署以及备份目录: ${basePath}`)
console.log(`备份后的文件夹名: ${bakDirName}`)
console.log('\n--------开始部署--------------\n')
doBakDir(serverCopy,basePathCopy,()=>{
doUploadDir(serverCopy, buildPath, `${basePathCopy}`, () => {
resolve();
console.log('\n--------部署成功--------------')
sendRobot(item)
})
})
})
}