wo-base-deployer/deploy.js

467 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require('fs')
const path = require('path')
/** ******************* 读取命令行以及配置文件里的参数 ******************** **/
const wo = (global.wo = {
envar: require('wo-base-envar').merge_envar({
rawEnvar: {
commanderOptions: [
// 命令行里可以接受的参数。将传给 commander。每个参数的定义格式是 [参数名,参数键,描述],后两者用于传给 commander取值后覆盖掉同名变量。
[
'fromPath',
'-f, --fromPath <fromPath>',
'local distribution path to copy from.'
],
[
'gotoTarget',
'-g, --gotoTarget <gotoTarget>',
'connection section name.'
],
[
'targetType',
'-t, --targetType <targetType>',
'target type, git or ssh.'
],
[
'host',
'-H, --host <host>',
'Host IP or domain name of the target server.'
],
['port', '-P, --port <port>', 'Ssh port number of the target server.'],
[
'targetPath',
'-d, --targetPath <targetPath>',
'Destination path to deploy on the target.'
],
[
'targetDir',
'-D, --targetDir <targetDir>',
'Destination folder to deploy on the target.'
],
['repo', '-r, --repo <repo>', 'git repo address.'],
['branch', '-b, --branch <branch>', 'git repo branch.'],
['gitname', '-n, --gitname <gitname>', 'git user name.'],
['gitemail', '-m, --gitemail <gitemail>', 'git user email.'],
['user', '-u, --user <user>', 'User id to login the target server.'],
[
'privateKey',
'-k, --privateKey <privateKey>',
'User private key file to login the target server.'
],
[
'password',
'-p, --password <password>',
'User password to login the target server. You may have to enclose it in "".'
]
],
// 最基础的必须的默认配置,如果用户什么也没有提供
deploy: {
fromPath: './_webroot',
gotoTarget: 'github',
server: {
targetType: 'ssh',
host: undefined,
port: 22,
targetPath: undefined, // 目标服务器上的目录。似乎该目录必须已经存在于服务器上
targetDir: '_webroot', // 新系统将发布在这个文件夹里。建议为dist和npm run build产生的目录一致这样既可以远程自动部署也可以直接登录服务器手动部署。
user: undefined,
password: undefined,
privateKey: `${process.env.HOME}/.ssh/id_rsa`
},
github: {
targetType: 'git',
repo: undefined,
branch: 'main',
gitname: undefined,
gitemail: undefined,
user: undefined,
password: undefined,
privateKey: `${process.env.HOME}/.ssh/id_rsa`
}
}
},
envarFiles: ['./envar-deploy.js', './envar-deploy-secret.js']
})
})
const envarDeploy = wo.envar.deploy
delete wo.envar.deploy
// 用 commander 采集到的配置 替换 文件中采集到的配置
envarDeploy.fromPath = wo.envar.fromPath || envarDeploy.fromPath
envarDeploy.gotoTarget = wo.envar.gotoTarget || envarDeploy.gotoTarget
// 使用用户指定的连接
const connection = envarDeploy[envarDeploy.gotoTarget]
Object.assign(connection, wo.envar) // 用 commander 采集到的配置 替换 文件中采集到的配置
connection.tryKeyboard = true
connection.onKeyboardInteractive = (
name,
instructions,
lang,
prompts,
finish
) => {
// 不起作用
if (
prompts.length > 0 &&
prompts[0].prompt.toLowerCase().includes('password')
) {
finish([password])
}
}
console.log(
`*** Deploy from ${envarDeploy.fromPath} to ${JSON.stringify(connection)} ***`
)
if (connection.targetType === 'ssh') {
deployToSsh(connection)
} else if (connection.targetType === 'git') {
deployToGit(connection)
}
/** ********************** 连接到 Ssh主机拷贝文件到指定路径 ************* **/
function deployToSsh (connection) {
const ssh = new (require('node-ssh'))()
function subDirs (path) {
const dirs = [path]
if (fs.statSync(path).isFile()) {
return dirs
}
fs.readdirSync(path).forEach(item => {
const stat = fs.statSync(`${path}/${item}`)
if (stat.isDirectory()) {
dirs.push(...subDirs(`${path}/${item}`))
}
})
return dirs
}
const necessaryPath = path => {
return subDirs(path)
.map(it => it.replace(path, ''))
.filter(it => it)
.map(it => it.split('/').filter(it => it))
}
ssh
.connect(connection)
.then(async () => {
console.log(
`[ mv ${connection.targetDir} ${
connection.targetDir
}-backup-${new Date().toISOString()} ... ]`
)
await ssh.execCommand(
`mv ${connection.targetDir} ${
connection.targetDir
}-backup-${new Date().toISOString()}`,
{ cwd: connection.targetPath }
)
console.log(`[ mkdir ${connection.targetDir} ... ]`)
await ssh.execCommand(`mkdir ${connection.targetDir}`, {
cwd: connection.targetPath
})
const toCreate = necessaryPath(path.join('./', envarDeploy.fromPath))
for (const name of toCreate) {
console.log(`[ mkdir ${connection.targetDir}/${name.join('/')} ... ]`)
await ssh.execCommand(
`mkdir ${connection.targetDir}/${name.join('/')}`,
{ cwd: connection.targetPath }
)
}
let err
console.log(
`[ Upload to ${connection.targetPath}/${connection.targetDir} ... ]`
)
await ssh.putDirectory(
path.join('./', envarDeploy.fromPath),
`${connection.targetPath}/${connection.targetDir}`,
{
concurrency: 10,
recursive: true,
validate: itemPath => {
const baseName = path.basename(itemPath)
return !baseName.endsWith('.map')
},
tick: (fromPath, remotePath, error) => {
console.log(
`Uploading "${fromPath}" ===> "${remotePath}" ${error ||
'succeeded!'}`
)
err = error
}
}
)
ssh.dispose()
if (err) {
console.error(
`🤷‍♀️🤷‍♀️🤷‍♀️ Failed deploy ${envarDeploy.fromPath} to ${connection.targetPath}/${connection.targetDir} 🤷‍♀️🤷‍♀️🤷‍♀️`
)
process.exit(1)
} else {
console.info(
`😊😊😊 Successfully deployed [${envarDeploy.fromPath}] to [${connection.targetPath}/${connection.targetDir}] 😊😊😊`
)
if (connection.url) {
console.info(`😊😊😊 ${connection.url} 😊😊😊`)
}
process.exit()
}
})
.catch(err => {
console.error(err)
ssh.dispose()
console.error(
`🤷‍♀️🤷‍♀️🤷‍♀️ Failed deploy [${envarDeploy.fromPath}] to [${connection.targetPath}/${connection.targetDir}] 🤷‍♀️🤷‍♀️🤷‍♀️`
)
process.exit(1)
})
}
/** ********************** 连接到 Git主机拷贝文件到指定路径 ************* **/
function deployToGit (connection) {
const pathFn = require('path')
const fs = require('hexo-fs')
const chalk = require('chalk')
const swig = require('swig-templates')
const moment = require('moment')
const Promise = require('bluebird')
const spawn = require('hexo-util/lib/spawn')
const swigHelpers = {
now: function (format) {
return moment().format(format)
}
}
const rRepoURL = /^(?:(?:git|https?|git\+https|git\+ssh):\/\/)?(?:[^@]+@)?([^\/]+?)[\/:](.+?)\.git$/ // eslint-disable-line no-useless-escape
const rGithubPage = /\.github\.(io|com)$/
function parseRepo (repo) {
const split = repo.split(',')
const url = split.shift()
let branch = split[0]
if (!branch && rRepoURL.test(url)) {
const match = url.match(rRepoURL)
const host = match[1]
const path = match[2]
if (host === 'github.com') {
branch = rGithubPage.test(path)
? connection.branch || 'main'
: 'gh-pages'
} else if (host === 'coding.net') {
branch = 'coding-pages'
}
}
return {
url: url,
branch: branch || 'main'
}
}
function parseConnection (args) {
const repo = args.repo || args.repository
if (!repo) throw new TypeError('repo is required!')
if (typeof repo === 'string') {
const data = parseRepo(repo)
data.branch = args.branch || data.branch
return [data]
}
const result = Object.keys(repo).map(key => {
return parseRepo(repo[key])
})
return result
}
function exec () {
const targetDir = ''
const deployDir = pathFn.join(targetDir, '.deploy_git')
const fromDir = envarDeploy.fromPath
let extendDirs = connection.extend_dirs
const ignoreHidden = connection.ignore_hidden
const ignorePattern = connection.ignore_pattern
const message = commitMessage(connection)
const verbose = !connection.silent
if (!connection.repo && process.env.HEXO_DEPLOYER_REPO) {
connection.repo = process.env.HEXO_DEPLOYER_REPO
}
if (!connection.repo && !connection.repository) {
let help = ''
help +=
'You have to configure the deployment settings in config files or command line first!\n\n'
help += 'Example:\n'
help +=
' node deploy.js -t git -r https://github.com/OWNER/OWNER.github.io -b main -f ./dist'
console.log(help)
return
}
function git (...connection) {
return spawn('git', connection, {
cwd: deployDir,
verbose: verbose,
stdio: 'inherit'
})
}
function setup () {
const userName = connection.gitname || ''
const userEmail = connection.gitemail || ''
// Create a placeholder for the first commit
return fs
.writeFile(pathFn.join(deployDir, 'placeholder'), '')
.then(() => {
return git('init')
})
.then(() => {
return userName && git('config', 'user.name', userName)
})
.then(() => {
return userEmail && git('config', 'user.email', userEmail)
})
.then(() => {
return git('add', '-A')
})
.then(() => {
return git('commit', '-m', 'First commit')
})
}
function push (repo) {
return git('add', '-A')
.then(() => {
return git('commit', '-m', message).catch(() => {
// Do nothing. It's OK if nothing to commit.
})
})
.then(() => {
return git('push', '-u', repo.url, 'HEAD:' + repo.branch, '--force')
})
.then(() => {
console.info(
`😊😊😊 Successfully deployed [${envarDeploy.fromPath}] to [${connection.repo}#${connection.branch}] 😊😊😊`
)
if (connection.url) {
console.info(`😊😊😊 ${connection.url} 😊😊😊`)
}
})
.catch(err => {
console.error(
`🤷‍♀️🤷‍♀️🤷‍♀️ Failed deploy [${envarDeploy.fromPath}] to [${connection.repo}#${connection.branch}] 🤷‍♀️🤷‍♀️🤷‍♀️`
)
process.exit(1)
})
}
return fs
.exists(deployDir)
.then(function (exist) {
if (exist) return
// log.info('Setting up Git deployment...')
return setup()
})
.then(() => {
// log.info('Clearing .deploy_git folder...')
return fs.emptyDir(deployDir)
})
.then(() => {
const opts = {}
// log.info('Copying files from local folder...')
if (typeof ignoreHidden === 'object') {
opts.ignoreHidden = ignoreHidden.public
} else {
opts.ignoreHidden = ignoreHidden
}
if (typeof ignorePattern === 'string') {
opts.ignorePattern = new RegExp(ignorePattern)
} else if (
typeof ignorePattern === 'object' &&
Reflect.apply(Object.prototype.hasOwnProperty, ignorePattern, [
'public'
])
) {
opts.ignorePattern = new RegExp(ignorePattern.public)
}
return fs.copyDir(fromDir, deployDir, opts)
})
.then(() => {
// log.info('Copying files from extend dirs...')
if (!extendDirs) {
return
}
if (typeof extendDirs === 'string') {
extendDirs = [extendDirs]
}
const mapFn = function (dir) {
const opts = {}
const extendPath = pathFn.join(targetDir, dir)
const extendDist = pathFn.join(deployDir, dir)
if (typeof ignoreHidden === 'object') {
opts.ignoreHidden = ignoreHidden[dir]
} else {
opts.ignoreHidden = ignoreHidden
}
if (typeof ignorePattern === 'string') {
opts.ignorePattern = new RegExp(ignorePattern)
} else if (
typeof ignorePattern === 'object' &&
Reflect.apply(Object.prototype.hasOwnProperty, ignorePattern, [dir])
) {
opts.ignorePattern = new RegExp(ignorePattern[dir])
}
return fs.copyDir(extendPath, extendDist, opts)
}
return Promise.map(extendDirs, mapFn, {
concurrency: 2
})
})
.then(() => {
return parseConnection(connection)
})
.each(function (repo) {
console.log('########## repo ###########')
console.log(repo)
return push(repo)
})
} // end of function exec
function commitMessage (connection) {
const message =
connection.m ||
connection.msg ||
connection.message ||
"Site updated: {{ now('YYYY-MM-DD HH:mm:ss') }}"
return swig.compile(message)(swigHelpers)
}
exec()
}