UNPKG

qspark-deploy

Version:

齐圣园区自动化部署

413 lines (371 loc) 12 kB
#!/usr/bin/env node 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) }) }) }) }