Skip to content

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)
  }
})()