Github webhook + Node Server + pm2 前端自动部署
- Published on
对前端 next.js 项目自动部署做的一次折腾尝试。
更加成熟的方案应该是
Docker Jenkins CI/CD
那一套。
这里 Next.js 项目运行通过 Node.js server 部署,脚本执行的执行也是通过 Node.js 完成的,所以本地和服务器都会强依赖 Node 环境~
思路以及要做的效果是这样:1.本地 git push
代码到github
--> 2. github webhook
调用请求Node server url
--> 3. 服务端脚本执行代码更新,编译和启动服务。
pm2 启动项目
pm2 是一个很强大的 Node 应用管理工具,保持应用后台运行再合适不过。包含了一些常用命令:list(ls), start, stop, reload, restart, delete
等等。
例如pm2 ls
列出 pm2 任务:

pm2
也可以运行npm
命令:
pm2 start npm --watch --name <taskname> -- run <scriptname>;
# 其中 -- watch监听代码变化,-- name重命名任务名称,-- run后面跟脚本名字
如经常执行的npm run dev
通过 pm2 启动就是 pm2 start npm -- run dev
, 等下就需要通过 pm2 执行 yarn build 和 yarn serve 来启动项目 server。(yarn serve 在 package.json 里对应的是 next start
)
配合 Node 的 child_process, 通过 Node 脚本执行 pm2 命令,可以在项目根目录下新建一个sitebuild.js
,做这几件事:
- pull 最新代码
- build 打包
- (移除后重新) 启动项目 server
const { execSync } = require('child_process')
function reDeployVMainsite(Callback) {
// __dirname目录下执行,加上输出便于调试
function execSyncCurDir(cmd) {
// console.info(`🕑 [${cmd} | Begins..]`)
execSync(cmd, { cwd: `${__dirname}` })
// console.info(`✨ [${cmd} -> Finish ! ]`)
}
try {
execSyncCurDir('git pull')
execSyncCurDir('yarn build')
try {
execSyncCurDir('pm2 delete vmainsite')
} catch (error) {
console.info('🌝 [vmainsite] not exist, start new one ~')
} finally {
execSyncCurDir('pm2 start yarn --name vmainsite -- serve')
}
Callback('[Start Finish]')
} catch (error) {
console.info('[error] - sync & build', error)
Callback('[Start Fail]' + error.toString())
}
}
// reDeployVMainsite()
module.exports = {
reDeployVMainsite,
}
pm2 可以 restart 一个任务,但是在任务停掉之后依然要经过 start,这里就用 delete 和 start 对启动任务和更新做统一处理
代码里将项目 server 命名为vmainsite
, 解开reDeployVMainsite
和log
注释,直接 Node 执行这个脚本,log 信息查看执行流程。执行完后就已经通过 pm2 运行了项目服务,可以通过pm2 list
命令查看vmainsite
是否存在, 没问题的话 localhost:3000
就可以访问到页面了。
这个 build.js 脚本的核心方法就是reDeployVMainsite
,在服务端监听到webhook
后得到调用,现在只是完成了第三步。如何监听 webhook?接着往下。
github webhook
这一部分的流程:github 仓库中有对应的 webhook 设置,配置一个 URL,在仓库中有一些push & star
等等动作触发后,github 会请求这个 URL, 带上仓库信息的 payload,自己创建一个 Node.js 服务处理这个请求,触发构建脚本任务。
gituhb webhook 配置
如下 URL 配置,请求的触发时机是可以设置的,仓库的任何事件或者仅仅是 push 时。

设置完成后,可以直接对仓库进行一次 push,然后查看页面上Recent Deliveries
中的记录,其中可以获取到具体的 payload 对象,这个里面的信息可以用作下面服务实现里的判断。
实现 Node.js webhook server
接着需要实现这个 payload URL 服务,主要通过 Node.js 的 http 模块编写, webhook-github.js:
/**
* 监听 github webhook push
*/
const http = require('http')
const url = require('url')
const fs = require('fs')
const { reDeployVMainsite } = require('../../vmainsite/sitebuild')
const apiPort = 9600
const resolvePost = (req) =>
new Promise((resolve) => {
let chunk = ''
req.on('data', (data) => {
chunk += data
})
req.on('end', () => {
resolve(JSON.parse(chunk))
})
})
const onRequest = async function (request, response) {
const parsedUrl = url.parse(request.url)
const pathname = parsedUrl.pathname
if (pathname === '/xxxx/xxxx') {
const payload = await resolvePost(request)
// console.info("[Payload]", payload);
// 分支信息
response.writeHead(200, { 'Content-Type': 'application/json' })
const result = {
resCode: 200,
msg: '开始构建',
}
response.end(JSON.stringify(result)) //将json传回浏览器
// 写入 log
if (payload.ref === 'refs/heads/xxxxx') {
setTimeout(() => {
fs.appendFileSync('./vmainsite.log', `\n[VMAIN PUSH] ${new Date().toLocaleString()}`)
reDeployVMainsite((error) => {
fs.appendFileSync('./vmainsite.log', ` ` + error)
})
}, 2000)
}
} else {
response.end('unHandle url:', request.url) //将json传回浏览器
}
}
const server = http.createServer(onRequest)
server.listen(apiPort, '127.0.0.1') //
console.log(`server started on localhost:${apiPort}`)
判断 pathname 为对应 payload URL 中的 pathname,也可以进一步判断 branch name, 在 payload 的 ref 中字段里(上面代码中 xxxx 替换为真实数据) ,导入上一步中编写的reDeployVMainsite
方法,在处理响应 response 后调用(在响应前同步调用会导致超时),同时可以向 log 中写入一些触发信息便于查看。
这个处理 github webhook 的服务通过 pm2 运行起来:
pm2 start webhook-github.js
到这里就结束了。完整链路是这样:
pm2
运行 webhook-github.js
,启动一个 Node.js webhook
服务,在需要自动部署的代码仓库里 push 到远端 github 的时候,就会触发请求设置的 payload URL
,我们的 Node.js webhook
服务接收处理这个请求,调用构建脚本中的 reDeployVMainsite
方法,开始 pull 最新代码然后打包,重新 serve。
运行完整个流程后,pm2 中就会两个任务,一个是监听的 webhook server
, 一个是项目自身的server(vmainsite)
。
