使用 GitHub Webhook 自动更新个人网站。bio-spring.top 是我的个人网站,其源代码托管在 GitHub 上。通过使用 GitHub Actions 可以自动构建网站并部署到 GitHub Pages,实现自动更新网站在 gaospecial.github.io/bio-spring/ 上的展示。通过使用 netlify 可以实现自动更新网站在 netlify 服务器上的展示。现在想要自动化部署到我的阿里云服务器上,该如何实现呢?
这里要用到几个部分的技术:
- GitHub Webhook
- 配置 Hook server
- 设置 Hook server 要执行的任务
配置 GitHub Webhook
GitHub Webhook 是一种自动化工具,它允许在 GitHub 仓库发生特定事件时向外部服务器发送通知。当配置的事件(如 push、pull request 等)发生时,GitHub 会向指定的 URL 发送 HTTP POST 请求,包含相关事件的详细信息。
访问 Webhook 设置
- 进入 GitHub 仓库
- 点击 “Settings” 选项卡
- 在左侧菜单中选择 “Webhooks”
- 点击 “Add webhook” 按钮
配置 Webhook
在添加 Webhook 时需要设置以下内容:
- 
Payload URL: 接收 webhook 请求的服务器地址 - 例如:http://webhook.bio-spring.top/
- 确保该 URL 可以从公网访问
 
- 例如:
- 
Content type: 选择数据传输格式 - 通常选择 application/json
 
- 通常选择 
- 
Secret: 设置密钥(可选但推荐) - 用于验证请求来源的合法性
- 确保安全传输
- 生成一个随机的秘钥字符串:openssl rand -hex 20
 
- 
事件触发选项: - “Just the push event”: 仅推送事件
- “Send me everything”: 所有事件
- “Let me select individual events”: 自定义选择事件
 
配置完成后,GitHub 会向指定的 URL 发送一个 ping 事件来测试连接。
配置 Hook server
上面配置完,会得到一个提示“Last delivery was not successful”,这是因为还没有配置 Hook server。接下来做这部分工作。
配置 Hook server 的 URL
要配置 webhook.bio-spring.top,需要在 Apache2 中添加一个虚拟主机配置。具体步骤如下:
- 
创建虚拟主机配置文件: <VirtualHost *:443> # 设置服务器名称 ServerName webhook.bio-spring.top ServerAlias webhook.bio-spring.top # 启用 SSL SSLEngine on SSLCertificateFile /etc/letsencrypt/live/webhook.bio-spring.top/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/webhook.bio-spring.top/privkey.pem # 设置日志路径 ErrorLog ${APACHE_LOG_DIR}/webhook-error.log CustomLog ${APACHE_LOG_DIR}/webhook-access.log combined # 设置文档根目录 DocumentRoot /var/www/html/webhook </VirtualHost>
- 
重启 Apache 服务: sudo a2ensite webhook.conf sudo service apache2 restart
- 
添加 DNS 解析 在 DNS 解析中添加 webhook.bio-spring.top 的解析,指向阿里云服务器的公网 IP 地址。 
- 
配置 https 证书 在阿里云服务器上安装 certbot 并配置 https 证书。 sudo apt install certbot sudo certbot --apache -d webhook.bio-spring.top
配置 Hook Server 的脚本
在 Hook Server 的文档根目录下创建一个 index.php 文件,内容如下:
<?php
// 配置
$secret = "your-secret-key";
$log_file = "/var/log/webhook/deploy-php.log";
$deploy_script = "/path/to/deploy-hook.sh";
// 记录日志
function write_log($message) {
    global $log_file;
    $date = date('Y-m-d H:i:s');
    file_put_contents($log_file, "[$date] $message\n", FILE_APPEND);
}
// 验证签名
function verify_signature($payload, $signature) {
    global $secret;
    $expected = "sha256=" . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}
// 确保是POST请求
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    die('Method Not Allowed');
}
// 获取GitHub签名
$headers = getallheaders();
$signature = isset($headers['X-Hub-Signature-256']) ? $headers['X-Hub-Signature-256'] : '';
if (empty($signature)) {
    write_log("Error: No signature provided");
    http_response_code(403);
    die('No signature');
}
// 获取请求体
$payload = file_get_contents('php://input');
// 验证签名
if (!verify_signature($payload, $signature)) {
    write_log("Error: Invalid signature");
    http_response_code(403);
    die('Invalid signature');
}
// 解析payload
$data = json_decode($payload, true);
// 检查是否是push事件
if (isset($data['ref']) && $data['ref'] === 'refs/heads/master') {
    write_log("Received push to master branch");
    
    // 执行部署脚本
    $output = [];
    $return_var = 0;
    exec("bash $deploy_script 2>&1", $output, $return_var);
    
    // 记录执行结果
    $output_str = implode("\n", $output);
    write_log("Deploy script output:\n$output_str");
    
    if ($return_var === 0) {
        write_log("Deploy completed successfully");
        echo "Deploy successful";
    } else {
        write_log("Deploy failed with code $return_var");
        http_response_code(500);
        echo "Deploy failed";
    }
} else {
    write_log("Ignored non-master push event");
    echo "Ignored";
}
?>
重启 Apache 服务器之后,访问 webhook.bio-spring.top 应该会返回 “Method Not Allowed”。
配置 deploy-hook.sh 脚本
#!/bin/bash
# 设置工作目录
SITE_DIR="/var/www/html/bio-spring.top"
LOG_FILE="/var/log/webhook/deploy-sh.log"
REPO_DIR="/home/git/bio-spring"
# 如果日志文件不存在则创建
if [ ! -f "$LOG_FILE" ]; then
    mkdir -p "$(dirname "$LOG_FILE")"
    touch "$LOG_FILE"
    chown www-data:www-data "$LOG_FILE"
    chmod 644 "$LOG_FILE"
fi
# 记录部署时间
echo "Deployment started at $(date)" >> $LOG_FILE
# 更新代码仓库
cd $REPO_DIR || exit
GIT_SSH_COMMAND='ssh -i /home/git/.ssh/id_rsa' git pull origin master >> $LOG_FILE 2>&1
GIT_SSH_COMMAND='ssh -i /home/git/.ssh/id_rsa' git submodule update --recursive >> $LOG_FILE 2>&1
# 安装 hugo
# wget https://github.com/gohugoio/hugo/releases/download/v0.136.5/hugo_0.136.5_linux-amd64.deb
# dpkg -i hugo_0.136.5_linux-amd64.deb
# 构建网站
Rscript -e 'blogdown::build_site(baseURL = "/")' >> $LOG_FILE 2>&1
# 同步到网站目录
rsync -av --delete public/ $SITE_DIR/ >> $LOG_FILE 2>&1
echo "Deployment completed at $(date)" >> $LOG_FILE
测试
在 GitHub 仓库中推送代码,应该会看到阿里云服务器上的网站自动更新。同时,GitHub Webhook 的日志中也会记录 delivery 信息。
注意:由于 GitHub Webhook 的超时时间为 10 s,如果超过 10 s 没有响应(脚本没有执行完毕),GitHub 的状态会显示为“time out”。这种情况可以通过异步执行脚本解决。或者也可以忽略。
异步执行脚本
在 PHP 中实现异步处理,可以使用 exec() 或 shell_exec() 命令来启动后台进程。通过在命令末尾添加 & 符号,可以让该进程在后台运行,而不阻塞主进程。
// 异步启动任务
exec("bash /path/to/deploy-hook.sh &");
以上代码会在后台执行 deploy-hook.sh,避免输出内容影响主进程。