使用 GitHub WebHook 实现 strapi 自动部署 | 青训营笔记

509 阅读2分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 7 天

青训营准备去完成防掘金前端的那个大项目,因为后端CMS不是重点,所以决定用 strapi 去快速实现。前几天简单看文档体验了一下,发现操作比较简单,而且内容管理起来非常方便。

经过多种配置的试验之后,决定使用sqlite作为 strapi 的数据库。比起mysql等需要网络连接的数据库,sqlite 在协作开发阶段就可以避免敏感的服务器连接方式与密钥在团队成员之间直接共享,而且数据库文件能够直接托管在git仓库中。

与 strapi 默认的策略不同,默认情况下, strapi 的并不推荐直接将数据库和媒体资源放到git仓库中托管。但是由于在开发阶段,strapi 对于我们来说更类似于 mock,事先把内容填充好,把媒体资源也一同托管在仓库中。也能够保证团队成员在各自开发时能够得到一致的后端响应。

这篇文章就主要谈谈我是如何使用 GitHub WebHook 来自动部署 strapi 并通知飞书群的。(虽然感觉自从GitHub Actions出来之后,WebHook不怎么被人提到了

提前准备

strapi 有 1k+ 依赖,部署时再去安装肯定不太行,所以先提前把后端仓库拉取到服务器,顺便把 Nginx 给配置好。守护进程用的是pm2,过程也很简单。

代码先拉取下来之后,npm i 安装依赖,然后使用pm2去运行develop命令

pm2 start --name juejin-cms npm -- run develop

服务就跑起来啦~

因为要保证线上环境的数据和仓库是同步的(即团队成员只能访问接口,而不能在线上环境增删内容),所以需要屏蔽掉 /admin 路由

server {
        # 忽略不重要的配置
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_set_header Accept-Encoding "";
        proxy_redirect off;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        location /admin {
                charset utf-8;
                default_type text/plain;
                return 200 "线上环境屏蔽管理面板";
        }

        location / {
                proxy_pass http://example.host/;
        }
}

脚本编写

为了方便之后的操作,写个bash脚本用来同步仓库和重启pm2服务

# autoCMSPull.sh

cd ./juejin-cms # 进入后端目录
git reset --hard origin/main >/dev/null # 重置分支状态
git clean -f >/dev/null # 清除没有追踪的文件
git pull # 拉取代码
pm2 restart juejin-cms >/dev/null # 重启服务

虽然线上环境屏蔽了管理面板,但是还是有一些接口具有写权限,能够修改信息,所以保险起见先使用 git reset --hard origin/maingit clean -f 把那些被修改的文件删除或重置掉,然后使用 git pull 拉取最新的代码,最后重启服务。

接下来就是写webhook的处理了,也比较简单。这里使用的是@octokit/webhooks库,对webhook的事件类型进行了封装,可以直接进行监听。

// deploy.js

const http = require('http')
const { Webhooks, createNodeMiddleware } = require("@octokit/webhooks");
const axios = require('axios')

const webhooks = new Webhooks({
    secret: "thisisfakesecret",
});

function RunCmd(cmd, args, cb) {
    const spawn = require('child_process').spawn;
    const child = spawn(cmd, args);
    const result = '';
    child.stdout.on('data', function (data) {
        result += data.toString();
    });
    child.stdout.on('end', function () {
        cb(result)
    });
}

http.createServer(createNodeMiddleware(webhooks)).listen(1234)

webhooks.on('push', function (event) {
    if (event.payload.repository.name === "juejin-cms") {
        var shpath = './autoCMSPull.sh';
        RunCmd('sh', [shpath], function (result) {
            console.log(result);
        })
    }
});

监听 push 事件时,如果检测到仓库名称是 juejin-cms 时,就执行刚刚的bash脚本。

为了实现消息推送,需要使用飞书的机器人接口,下面是示例代码

const sendPushMsg = (payload) => {
    const api = "https://open.feishu.cn/open-apis/bot/v2/hook/[uuid]"

    const { ref, head_commit, repository } = payload
    const { name, url } = repository
    const { message, author, url: commitUrl } = head_commit
    const { name: authorName } = author

    const data = {
        "msg_type": "interactive",
        "card": {
            "header": {
                "title": {
                    "content": "后端仓库新的提交",
                    "tag": "plain_text"
                }
            },
            "elements": [{
                "tag": "div",
                "text": {
                    "content": `**仓库:**${name}\n**分支:**${ref}\n**提交信息:**${message}\n**提交人:**${authorName}`,
                    "tag": "lark_md"
                },
            },
            {
                "tag": "div",
                "text": {
                    "content": `已自动部署至 [https://example.com](https://example.com)`,
                    "tag": "lark_md"
                },
            },
            {
                "tag": "action",
                "actions": [{
                    "tag": "button",
                    "text": {
                        "content": "查看提交",
                        "tag": "plain_text"
                    },
                    "url": commitUrl,
                    "type": "default"
                },
                {
                    "tag": "button",
                    "text": {
                        "content": "查看仓库",
                        "tag": "plain_text"
                    },
                    "url": url,
                    "type": "default"
                }]
            }]
        }
    }

    axios.post(api, data).then(res => {
        console.log(res.data)
    }).catch(err => {
        console.log(err)
    })
}

image.png