Appearance
deploy.cjs
js
const { createReadStream, createWriteStream, unlinkSync } = require('fs')
const { ZipArchive } = require('archiver')
const { Client } = require('ssh2')
const { projectName, config } = require('./config.cjs')
// 压缩成 zip
const zipFolder = () => {
return new Promise((resolve, reject) => {
const output = createWriteStream(config.localZip)
const archive = new ZipArchive({ zlib: { level: 6 } })
output.on('close', () => {
console.log(
`✅ 本地压缩完成,大小:${(archive.pointer() / 1024 / 1024).toFixed(2)} MB`
)
resolve()
})
archive.on('error', err => reject(err))
archive.pipe(output)
// 第二个参数表示文件夹内的资源放到该名称目录下
archive.directory(config.localDir, projectName)
archive.finalize()
})
}
// ssh + sftp上传 + 远程备份、解压、删包
const uploadAndDeploy = () => {
return new Promise((resolve, reject) => {
const conn = new Client()
conn
.on('ready', () => {
console.log(`✅ SSH 连接成功,开始上传 ${projectName}.zip`)
conn.sftp((err, sftp) => {
if (err) return reject(err)
const rs = createReadStream(config.localZip)
const ws = sftp.createWriteStream(config.remoteZip)
ws.on('close', () => {
console.log('✅ 上传完毕,执行远程部署命令')
// [ -d "${config.targetDir}" ] 表示目录存在才备份
// 部署后端时可以在最后加上 && pm2 restart projectName
const cmd = `cd "${config.targetParentDir}" \
&& [ -d "${config.targetDir}" ] && mv "${config.targetDir}" "${config.targetDir}_bak_\$(date +%Y%m%d_%H%M%S)" \
&& unzip -o "${config.remoteZip}" > /dev/null 2>&1 \
&& rm -f "${config.remoteZip}"`
conn.exec(cmd, (err, stream) => {
if (err) return reject(err)
stream.on('close', code => {
if (code === 0) {
console.log('🎉 远程部署全部完成(备份 + 解压 + 删 zip)')
resolve()
} else {
reject(new Error(`远程命令退出码 ${code}`))
}
conn.end()
})
stream.on('data', d => console.log('stdout:', d.toString()))
stream.stderr.on('data', d =>
console.log('stderr:', d.toString())
)
})
})
rs.pipe(ws)
})
})
.on('error', e => reject(e))
.connect(config)
})
}
;(async () => {
try {
await zipFolder()
await uploadAndDeploy()
// 部署成功后删除本地 zip
unlinkSync(config.localZip)
} catch (err) {
console.error('❌ 失败:', err.message)
process.exit(1)
}
})()