使用 Bash 编写 GitHub Web Hook

作者:Andy Carlson

将您的 GitHub 仓库提升到新的功能水平。

自从微软收购 GitHub 以来,过去一年我一直将我的 Git 仓库托管在私有服务器上。虽然我很享受设置这一切带来的机会和挑战,并且最终产品也很好地满足了我的需求,但这样做并非没有牺牲。 GitHub 提供了一个简洁的界面来配置许多 Git 功能,否则这些功能将需要比简单点击按钮更多的时间和精力。我最喜欢的 GitHub 简化实现的功能之一是 Web Hook。当 GitHub 应用程序中发生特定事件时,就会执行 Web Hook。执行后,数据将通过 HTTP POST 发送到指定的 URL。

本文将介绍如何设置自定义 Web Hook,包括配置 Web 服务器、处理来自 GitHub 的 POST 数据以及使用 Bash 创建一些基本的 Web Hook。

准备 Apache

为了本项目的目的,让我们使用 Apache Web 服务器来托管 Web Hook 脚本。 Apache 用于运行服务器端 shell 脚本的模块是 mod_cgi,该模块在主要的 Linux 发行版上都可用。

启用该模块后,就该配置 Apache 中的目录权限和虚拟主机了。使用 /opt/hooks 目录来托管 Web Hook,并将此目录的所有权授予运行 Apache 的用户。要确定运行 Apache 实例的用户,请运行以下命令(前提是 Apache 当前正在运行)


ps -e -o %U%c| grep 'apache2\|httpd'

这些命令将返回一个两列输出,其中包含运行 Apache 的用户的名称和 Apache 二进制文件的名称(通常为 httpdapache2)。使用以下 chown 命令授予目录权限(其中 USER 是先前 ps 命令中显示的用户名称)


chown -R USER /opt/hooks

在此目录中,将创建两个子目录:html 和 cgi-bin。 html 文件夹将用作虚拟主机的 Web 根目录,cgi-bin 将包含虚拟主机的所有 shell 脚本。请注意,随着在 /opt/hooks 下创建新的子目录和文件,您可能需要重新运行上述 chown 以验证对文件和子目录的正确访问权限。

以下是 Apache 中虚拟主机的配置


<VirtualHost *:80>
  ServerName SERVERNAME
  ScriptAlias "/cgi-bin" "/opt/hooks/cgi-bin"
  DocumentRoot /opt/hooks/html
</VirtualHost>

ServerName 指令的值从 SERVERNAME 更改为将通过 Web Hook 访问的主机的名称。此配置提供了托管文件和执行 shell 脚本的基本功能。 DocumentRoot 指令使用本地系统上的绝对路径指定虚拟主机的根目录。 ScriptAlias 指令接受两个参数:虚拟主机中的绝对路径和本地系统上的绝对路径。虚拟主机中的路径映射到本地系统路径。 mod_cgi 处理对 ScriptAlias 指令中指定的路径发出的所有请求。(注意:本文未涵盖包括 SSL 或日志记录在内的任何其他配置。)

CGI 基础知识

您需要对 HTTP 协议和 Bash 脚本编写有基本的了解,才能理解 CGI 脚本的工作原理。当向 HTTP 服务器发出请求时,会生成响应并发送回客户端。 HTTP 请求包含指示服务器如何处理请求的标头。同样,HTTP 响应包含指示客户端如何处理响应的标头。使用任何现代浏览器上的开发者工具可以非常简单地查看和分析 HTTP 流量。以下是一个简单的 HTTP 请求和响应示例

请求


POST /cgi-bin/clone.cgi HTTP/1.1
Host: hooks.andydoestech.com
Content-length: 86

{"repository":{"name":webhook-test","url":https://github.com/
↪bng44270/webhook-test"}}

响应


HTTP/1.1 200 OK
Date: Tue, 11 Jun 2019 02:44:52 GMT
Content-Length: 18
Content-Type: text/json

{"success":"true"}

该请求正在向位于 http://hooks.andydoestech.com//cgi-bin/ 中的 clone.cgi 文件发出 POST 请求。响应包含响应代码、处理请求的日期/时间、内容主体长度(以字节为单位)以及内容主体本身。虽然有时可能会通过 HTTP 发送二进制数据,但本文中的示例仅处理明文传输。

鉴于 Bash 具有强大的文本处理能力和可用命令,它非常适合构建和操作 HTTP 事务中的文本。如果上述 HTTP 请求要由 Bash 脚本处理,则它可能如下所示


#!/bin/bash

JSONPOST="$(cat -)"

echo "Date: $(date)"
echo "Content-Length: 18"
echo "Content-Type: text/json"
echo ""
echo "{\"success\":\"true\"}"

虽然此脚本缺乏逻辑,但它很好地说明了 HTTP POST 数据是如何作为 JSONPOST 变量捕获的,以及 HTTP 响应标头和数据是如何通过标准脚本输出返回给客户端的。

解析 JSON

虽然许多 GitHub 资源可以触发 Web Hook,但本文特别关注推送事件,该事件在将数据远程推送到代码仓库时触发。当发出 Web Hook 的 HTTP POST 请求时,JSON 对象将发布到 URL。此 JSON 对象包含许多与推送操作相关的信息,包括有关仓库和数据推送中包含的提交的信息。用于从 POST JSON 中解析单个值的命令是 jq,该命令在主要的 Linux 发行版上都可用。该命令的语法要求以点表示法指定所需的属性。例如,考虑以下从 GitHub 返回的 JSON 对象片段


{
  "repository": {
    "name": "webhook-test",
    "git_url": "git://github.com/bng44270/webhook-test.git",
    "ssh_url": "git@github.com:bng44270/webhook-test.git",
    "clone_url": "https://github.com/bng44270/webhook-test.git",
  }
}

要使用 jq 返回名为 clone_url 的属性的值,您将使用以下语法


jq -r '.repository.clone_url' <<< 'JSON'

在用 JSON 对象的文本表示形式替换 JSON 后,此命令将返回 HTTP 仓库克隆 URL。使用命令替换,可以将 JSON 属性的值分配给 Bash 变量,以便在脚本中使用。

Hook #1:简单备份

我想介绍的第一个 Hook 将在托管 Web Hook 脚本的 Apache 服务器上创建仓库的备份。本示例将使用上述 VirtualHost 配置。以下是仓库备份 Web Hook 脚本


1  #!/bin/bash
2
3  REPODIR="/opt/hooks/html/repos"
4
5  json_resp() {
6       echo '{"result":"'"$([[ $1 -eq 0 ]] && echo "success"
 ↪|| echo "failure")"'"}'
7  }
8
9  POSTJSON="$(cat -)"
10
11 REPOURL="$(jq -r ".repository.clone_url" <<< "$POSTJSON")"
12 REPONAME="$(jq -r ".repository.name" <<< "$POSTJSON")"
13
14 echo "Content-type: text/json"
15 echo ""
16
17 if [ -d $REPODIR/$REPONAME ]; then
18      pushd .
19      cd $REPODIR/$REPONAME
20      git pull
21      json_resp $?
22      popd
23 else
24      mkdir $REPODIR/$REPONAME
25      git clone $REPOURL $REPODIR/$REPONAME
26      json_resp $?
27 fi

脚本开头的 REPODIR 变量指示将包含所有仓库目录的目录。 json_resp 函数允许在脚本中多次重用生成 JSON 响应的代码。与上面的示例一样,HTTP POST 数据被捕获在 POSTJSON 变量中。在第 11 行和第 12 行中,使用 jqPOSTJSON 变量中提取 clone_url 和 name 属性。第 14 行开始创建 HTTP 响应标头。第 17-27 行的 if 块确定仓库是否已被克隆。如果已被克隆,则脚本移动到仓库文件夹,拉取仓库更改,然后返回到原始工作目录。如果该文件夹不存在,则创建该目录,并将仓库克隆到新目录。请注意使用在脚本开头设置的 $REPODIR 变量。无论仓库是克隆还是更新被拉取,都会调用 json_resp 函数来生成响应 JSON,其中将包含一个名为“success”的属性,其值为“true”或“false”,具体取决于相应 git 命令的结果。

Hook #2:构建和打包

备份仓库可能很有用。凭借命令行上可用的众多构建工具,创建 Web Hook 来交付仓库中代码的构建包是有意义的。这可以构建成一个强大的解决方案,满足持续集成/部署 (CI/CD) 的需求。以下是构建/部署 Web Hook 脚本


1  #!/bin/bash
2
3  WEBROOT="/opt/hooks/html/archive"
4  REPODIR="/opt/hooks/html/repos"
5  WEBURL="http://hooks.andydoestech.com/archive"
6
7  json_package() {
8       echo '{"result":"'$([[ $1 -eq 0 ]] && echo
 ↪"\"success\",\"url\":\"$1\"" ||
 ↪echo "\"package failure\"")"'}'
9  }
10
11 run_make() {
12      [[ -d $REPODIR/$REPONAME/build ]] && make -s -C
 ↪$REPODIR/$REPONAME clean
13      if [ $1 -eq 0 ]; then
14              make -s -C $REPODIR/$REPONAME
15              if [ -d $REPODIR/$REPONAME/build ]; then
16                      FILENAME="$REPONAME-$COMMITTIME.tar.gz"
17                      tar -czf $WEBROOT/$FILENAME -C
 ↪$REPODIR/$REPONAME/build .
18                      json_package "$?" "$WEBURL/$FILENAME"
19              else
20                      echo '{"result":"build failure"}'
21              fi
22      else
23              echo '{"result":"clone/pull failure"}'
24      fi
25 }
26
27 POSTJSON="$(cat -)"
28
29 REPOURL="$(jq -r ".repository.url" <<< "$POSTJSON")"
30 REPONAME="$(jq -r ".repository.name" <<< "$POSTJSON")"
31 COMMITTIME="$(jq -r '.commits[0].timestamp' <<<
 ↪"$POSTJSON" | date -d "$(cat -)" +"%m-%d-%YT%H-%M-%S")"
32
33 echo "Content-type: text/json"
34 echo ""
35
36 if [ -d $REPODIR/$REPONAME ]; then
37      pushd .
38      cd $REPODIR/$REPONAME
39      git pull
40      run_make $?
41      popd
42 else
43      mkdir $REPODIR/$REPONAME
44      git clone $REPOURL $REPODIR/$REPONAME
45      run_make $?
46 fi

与 Hook #1 类似,变量在脚本开头定义,以指定将克隆仓库的目录、将存储构建包的目录以及构建包的基本 URL。在第 7-25 行定义的两个函数将在脚本后面使用。第 27-31 行正在捕获 JSON POST 数据,并使用 jq 将属性解析为 shell 变量。请注意,COMMITTIME 中的日期格式正在从其原始形式修改(稍后这将变得有意义)。第 33-46 行在设置 HTTP 标头和克隆/拉取仓库方面与 Hook #1 几乎相同,但添加了对 run_make 函数的调用。克隆/拉取的返回状态传递给 run_make 函数。如果克隆/拉取成功运行,则该函数假定仓库的根目录中存在 Makefile。假定 Makefile 的行为方式如下

  • 当执行 make 时,解决方案将构建到仓库中名为“build”的文件夹中。
  • 当执行 make clean 时,“build”文件夹将被删除。

从第 12 行开始,如果构建文件夹存在,则执行 make clean 以将其删除。如果第 13 行的 make 成功,则使用 REPONAMECOMMITTIME 构造存档文件名。请注意,COMMITTIME 的值不包含空格,以便获得正确的文件名。第 17 行的 tar 命令的状态代码将传递到 json_package 函数。如果成功创建了存档,则定义一个包含两个 JSON 属性的 JSON 对象:result 设置为“success”,url 设置为存档的 URL。如果无法创建存档,则 result 属性设置为“package failure”。

GitHub 提供了许多功能,但毫无疑问,Web Hook 为 DevOps 工程师提供了完成几乎任何任务的工具。以这种方式利用 Apache 与 CGI 和 Bash 脚本编写的功能,使其可以被 GitHub 使用,从而实现了几乎无限的可能性。

资源

有关本文中提及的主题的更多信息,请参阅以下链接

Andy Carlson 在 IT 行业工作了 15 年,从事网络和服务器管理以及偶尔的编码工作。他很庆幸自己选择了自己热爱、成长和学习的职业。他目前与妻子、三个女儿和一个儿子居住在俄亥俄州辛辛那提市。他的家人目前正在进行国际收养两个孩子的过程。他喜欢弹吉他、编码以及与家人和朋友共度时光。

加载 Disqus 评论