namelesscoder / gizzle
GitHub Webhook 监听器,具有基于插件的 API,用于创建自定义触发动作
Requires
- milo/github-api: *@stable
- symfony/yaml: *@stable
Requires (Dev)
- phpunit/phpunit: @stable
- satooshi/php-coveralls: @stable
README
一个用 PHP 编写的微型 GitHub Webhook 监听器,可以轻松通过插件进行扩展。
安装 Gizzle
运行
composer require namelesscoder/gizzle
假设你的项目使用 composer 类加载器,那么你可以访问 Gizzle 的类在你的项目中。
虚拟主机
如果你希望使用默认的接收脚本,则从 Gizzle 的 web
文件夹创建符号链接
ln -s vendor/namelesscoder/gizzle/web
然后为你的 favorite HTTP 服务器添加一个(公开可访问!)虚拟主机,指向此文件夹内 ./web/
中的 ./web/
或者在你的 composer 应用程序中包含此包。
最后,配置你的 GitHub 仓库,并添加指向你的 Gizzle 项目的 URL,指向文件 /github-webhook.php
,如通过 HTTP 请求所述(文件系统中的 web/github-webhook.php
)或配置虚拟主机以默认提供 github-webhook.php
文件名。
当然,如果你需要额外的功能,可以创建自己的接收脚本而不是 github-webhook.php
。
注意!默认接收脚本仅支持本 README 文件中描述的功能。
安全(密钥文件和个人令牌)
Gizzle 使用在 GitHub 设置 web hook 时输入的 secret
(一个令牌)。在初始化 Payload 类时使用相同的密钥令牌。密钥令牌是必需的,并且必须匹配,否则 Payload 会抛出 RuntimeException。
当使用随附的公开文件 ./web/github-webhook.php
时,你的密钥令牌将从文件 ./.secret
中读取(注意:点文件,放置在公共 Web 根目录之外)。在首次安装此软件包或使用此软件包的自己的软件包时,请以任何你喜欢的任何方式创建该文件,并确保它包含你的“secret”密钥从 GitHub。例如,使用 shell:
echo "mysupersecretkey" > .secret
Gizzle 还可以使用个人访问令牌与 GitHub 进行“对话”。你可以利用此功能来进行诸如在提交上评论和更新状态(当提交是拉请求的一部分时显示)的操作——甚至可以自动合并分支。每个插件的默认实现都将 自动更新提交状态为挂起、成功或错误,随着 Payload 执行的进展,但其他插件可以访问 Gizzle 插件中的 GitHub API。你只需要确保存在一个特殊的 .token
文件,并包含你的个人访问令牌——这就像 .secret
文件一样,是敏感信息,你不应该共享。该 .token
文件以与 .secret
文件相同的方式创建,你可以为 Gizzle 使用新的访问令牌。 有关如何使用 GitHub API 集成的更多详细信息,请参阅本节。
运行 Gizzle
与此存储库一起提供的 ./web/github-webhook.php
文件可以作为配置 GitHub 时的 Webhook URL 使用——或者你可以手动处理你的应用程序中的有效负载并使用其 URL。
$data = file_get_contents('php://input'); $secret = 'mysecret'; $gizzle = new \NamelessCoder\Gizzle\Payload($data, $secret); // Plugins are then loaded from the packages used in, and in the order of, file `settings/Settings.yml` (see below) // * alternative loading 1: $gizzle->loadPlugins('MyVendor\\MyPackage'); // * alternative loading 2: $gizzle->loadPlugins($arrayOfPackageNames); // * alternative loading 3: $gizzle->loadPlugins($package1, $package2, $package3); // using either alternative causes the all plugins returned by the PluginList to be ran. Settings are still applied // but not used for determining plugins to run and in which order, as it is when running a settings file. /** @var \NamelessCoder\Gizzle\Response $response */ $response = $gizzle->process(); /** @var integer $code */ $code = $response->getCode(); // code >0 indicates errors are present; value indicates exact error. Code =0 means no errors. /** @var \RuntimeException[] $errors */ $errors = $response->getErrors();
配置插件
并非所有插件都支持配置,但支持配置的插件可以通过在项目的根目录或任何文件夹中放置一个 Settings.yml
文件来配置,该文件夹通向创建 Payload 实例的文件。在反向搜索中找到的第一个文件将被使用,并且应包含所有插件所需的配置。
配置文件的格式如下
Vendor\PackageName: Vendor\PackageName\GizzlePlugins\OnePlugin: enabled: true AnotherVendor\AnotherPackageName\GizzlePlugins\OtherPlugin: enabled: true OtherVendor\ThirdPackage: OtherVendor\ThirdPackage\GizzlePlugins\ThirdPlugin: enabled: true
任何包都可以返回来自其他包的插件,这在构建包含并提供多个插件配置的包时是理想的。这样的包可能返回不属于该包的插件名称,并且因为类名用作配置中的键,所以当由包A和包B提供时,可以以不同的方式配置相同的插件。这是为了提高小插件的复用性,这些插件可以服务于许多目的——一个很好的例子是Git插件,当在一个包中使用时,会推送,而在另一个包中使用时,则会拉取,因为每个包都可以为相同的插件提供不同的配置。
不过要小心:插件只能在返回它的包的作用域内进行配置。这意味着每个地方都应包含每个强制选项(换句话说:你不能配置全局默认值)。
插件事件
Gizzle为您提供了为每个插件配置在特殊事件上应与相同有效负载一起执行的其他插件列表的方法。目前支持的是onStart
、onSuccess
和onError
事件。有两种配置事件的方法;一种是在您的自定义插件类内部,通过提供对应设置的默认值(您可以在initialize()
或getSetting
方法中这样做,您可以选择您喜欢的)——另一种方法是在您的配置文件中定义这些设置。如果从自定义插件提供这些作为默认设置值,只需返回每个事件在此示例中表达的确切相同的数组即可。
关于示例:该配置配置了自更新插件,该插件在其配置中运行的包的根目录中执行git pull
和composer install
。还使用了另一个子插件,这是一个虚构的插件,它将邮件发送到推送描述在有效负载中的提交的实体,cc:或bcc:由该插件的设置定义,以及另一个虚构的插件,它能够以自定义严重性向syslog
发送消息。它还展示了您如何使用它,以您想要的任何嵌套级别,在每个级别捕获事件;在这种情况下,如果自更新失败,并且推送者或主开发人员无法直接通知失败,则会引发“CRITICAL!”syslog消息。
maxMessages: 3 MyVendor\MyGizzleImplementingPackage: NamelessCoder\Gizzle\GizzlePlugins\SelfUpdatePlugin: onStart: # send a copy to pusher only, informing that self-update began OtherVendor\GizzleEmailPlugins\GizzlePlugins\EmailPusherPlugin: subject: Starting self-update of MyGizzleImplementingPackage onError: # send a copy to pusher and bcc an address that won't be disclosed to pusher OtherVendor\GizzleEmailPlugins\GizzlePlugins\EmailPusherPlugin: subject: Your team updated a service, please validate the result. bcc: master-devops@organization.foo onError: OtherVendor\MonitoringPlugins\GizzlePlugins\LogPlugin: message: CRITICAL ERROR! Man all battle stations, the server can't send mail! severity: fatal onSuccess: # send a copy to pusher with cc to all developers OtherVendor\GizzleEmailPlugins\GizzlePlugins\EmailPusherPlugin: subject: MyGizzleImplementingPackage was updated! cc: developers@organization.foo onFinish: # event executed after the error/success event regardless of outcome # comment on HEAD commit that Gizzle finished processing; include output lines/errors. NamelessCoder\Gizzle\GizzlePlugins\CommentPlugin: comment: Finished processing commit: true
您可以在每个设置文件中通过在NamelessCoder\Gizzle
作用域下配置Gizzle的少量设置。
请注意,事件是在与任何其他插件使用的设置相同的级别上定义的,这意味着名称是保留的,不能用作您自己的插件设置的名称。定义一个错误的值(不是一个数组)可能会导致异常。然而:与“root”插件报告错误的方式相反,由用作事件监听器的插件引发的异常将简单地添加一个友好的错误消息到响应中,允许下一个插件继续,依此类推。而“root”插件将导致整个有效负载停止处理。
多个配置
您可以创建任意数量的替代配置文件。在实例化有效负载时,可以手动提供设置文件的名称或路径。
$settingsFile = 'Settings/SpecialSettings.yml'; $payload = new Payload($data = '{}', $secret = '', $settingsFile);
然后您可以将任意数量的此类附加设置文件放置在./Settings/*.yml
中,并通过更改第三个参数来引用它们。
与Gizzle一起提供的默认实现——位于./web/
中的github-webhook.php
文件——使用$_GET['settings']
参数作为第三个参数,在验证它只包含允许的字符:a-z
、A-Z
、0-9
和/
之后。允许使用后者是为了让您将配置文件分成任意数量的子目录,并通过路径引用它们。此外,文件名本身必须以.yml
结尾,不能是隐藏文件(点文件),最后必须是相对路径(例如,不以/
开头)。
要选择使用哪种配置,只需在将URL作为GitHub仓库设置中的webhook添加时设置预期的GET参数。
http://mydomain.foo/github-webhook.php?settings=Settings/SpecialSettings.yml
您还可以指定多个设置文件,所有这些文件都必须通过指定数组作为参数来处理。
http://mydomain.foo/github-webhook.php?settings[]=Settings/SpecialSettings.yml&settings[]=Settings/OtherSettings.yml
这将导致首先处理./Settings/SpecialSettings.yml
,然后是./Settings/OtherSettings.yml
,在相同的执行中。任何由一个文件引起的错误都将导致工作退出,只抛出第一个发生的错误。
您还可以使用此功能进行设置版本管理。例如,如果您的设计实践发生变化,并且您需要支持超过一个仓库设计模式,您可以轻松地将旧配置存储为不同的设置文件,并通过修改每个仓库的webhook URL,同时支持您两个仓库的模式。这种版本控制可能成为必要的情况之一是在“git flow”模式之间切换,或在多个生产分支场景中,新生产分支不断添加和删除。
插件中API的使用
API可以通过提供给插件基本方法的$payload
参数来访问。要访问API
$api = $payload->getApi(); if (TRUE === empty($api->getToken()) { // avoid doing anything with the API when there is // no token loaded; UNLESS you are able to provide // your own token from inside the plugin code. } else { $response = $api->get('/emoji'); $emojis = $api->decode($response); }
GitHub中的每个资源类型都有自己的URL,并且某些资源类型根据上下文支持不同的参数 - 例如,GitHub中的“评论”可以在几个不同的上下文中创建,并且每个上下文都有对所需属性的特定要求。请参阅GitHub API v3开发者文档以了解每个资源的详细信息。
请注意,当您需要从响应中读取数据时,需要额外的解码步骤。返回了一个具有公共属性的stdClass
,允许您读取响应数据 - 请参阅官方GitHub v3 API参考以了解每个操作可用的数据。如果您需要其他数据类型,请手动使用json_decode($response->getContent());
并传递您需要的任何特殊JSON_*
选项。
提示:如果API返回包含例如提交、仓库、实体等数据的数组类型数据,可以通过将属性数组简单地传递给构造函数来通过Gizzle的领域模型映射此类数据(递归):
$commit = new Commit($commitDataAsArray);
。
更新提交状态
如果您希望Gizzle在处理过程中更新Payload的HEAD提交(每个处理设置文件的设置文件一个状态),Gizzle支持一个GitHub个人访问令牌,它与.secret
文件类似,放在项目根目录中,命名为.token
。令牌是一个32字符的字符串,包含随机字母和数字。
要获取个人访问令牌
- 在GitHub中,在账户设置下,生成一个新的访问令牌。
- 确保您将此令牌与至少可以访问状态和访问(公共)仓库的权限关联起来。如果任何插件需要额外的权限,每个插件都应该记录哪些权限,并且您应该根据需要添加权限。
- 复制令牌并将其插入到项目根目录中的
.token
文件中。 - 不要提交令牌文件!相反,将其添加到本地或项目的gitignore中。令牌是敏感信息,绝不应共享。
当存在时,令牌将从此文件中读取并用于初始化Gizzle使用的GitHub API。然后,Gizzle和Gizzle插件可以使用API执行您允许的任何操作。
使用Github API的额外资源
为提交或拉取请求(或其中的文件)添加注释
Gizzle包含一个简单的Message对象,可用于向GitHub发送注释,并将它们附加到提交或拉取请求,或者特定文件中提交或拉取请求的某一行。尽管可以使用GitHub API手动发送此类消息(仍然可行!),但使用Message对象可以控制消息流,避免重复注释。例如,消息将被暂存,您可以设置暂存中允许的最大消息数,以防止洪泛。如果超出限制(对于当前的Payload,无论正在处理多少设置文件),Gizzle将用一条消息替换所有暂存的消息,告知报告的项目太多,并附带生成的消息的摘要。
尽管这些消息允许用户几乎立即获得反馈,例如创建拉取请求的用户,但它并不打算替代持续集成解决方案。使用消息返回的理想反馈类型是正式性检查:提交消息风格、拉取请求构成(例如,警告将所有提交合并)和简短的代码风格检查。请注意,访问文件的实际内容需要从手动签出的存储库或使用GitHub的原始数据URL(如果您使用的实用程序不支持检查URL目标,您需要手动下载到临时文件)读取每个文件的每个文件的内容。
您可以使用这种方式为通过GitHub网页界面提交拉取请求的用户提供几乎即时的正式性反馈。但是:您应该考虑在用户将更改推送到远程或甚至创建提交之前,在存储库中安装实际的git钩子以执行这些检查。通过GitHub网页界面创建的拉取请求时不会处理这些钩子 -但它为任何在克隆的存储库中对本地更改的用户提供了优秀且即时的反馈。
有关如何实现此类钩子的示例,请参阅https://github.com/FluidTYPO3/fluidtypo3-development,其中包含验证提交消息、运行单元测试和风格检查的示例,并包含一个维护钩子的脚本(而不是要求用户在更新时复制每个钩子)。
要创建并发送一个最终成为GitHub评论的新消息,您只需要访问Payload
实例
$message = new NamelessCoder\Gizzle\Message( 'This is an inline message for a line in a file', '/path/to/file.txt', 123 ); $message->setCommit($payload->getHead()); $payload->sendMessage($message);
并且要创建一个更通用的评论,该评论将添加到GitHub拉取请求视图中的“讨论”选项卡
$message = new NamelessCoder\Gizzle\Message( 'This is a comment for the pull request itself' ); $message->setPullRequest($payload->getPullRequest()); $payload->sendMessage($message);
最后,如果指定了拉取请求和提交,Gizzle假定您的意图是在拉取请求中为该提交创建内联审查评论
foreach ($payload->getCommits() as $commit) { // Example loop. An implementation like this one would for example // pull information from $commit and validate the message or read // a list of files changed and run a code style check directly on // GitHub's "raw" hosted file or download the file and then check it. $message = new NamelessCoder\Gizzle\Message( 'This is an inline review comment', '/path/to/file', 123 ); $message->setPullRequest($payload->getPullRequest()); $message->setCommit($commit); $payload->sendMessage($message); }
然后处理暂存的Message实例,这是在所有插件执行完毕后的最后。
创建插件
要为Gizzle创建插件,您需要一个必需的类和可选的列表器类(该类由您的包中的所有插件共享)
- 类
MyVendor\MyPackage\GizzlePlugins\MyAwesomePlugin
(或另一个类名或命名空间位置 - 由您选择)实现NamelessCoder\Gizzle\PluginInterface
及其指定的方法。 - 可选的还有类
MyVendor\MyPackage\GizzlePlugins\PluginList
,它必须实现接口NamelessCoder\Gizzle\PluginListInterface
,并包含一个getPluginClassNames
方法,该方法返回任何数量的插件字符串类名数组。当用户使用包名称引用您的插件集合时使用此类 - 如果您的插件仅打算在Settings.yml
上下文中使用(如上所述),则不需要此类。
当用户通过包名加载您的插件时,您的PluginList类会被要求返回插件的类名,正是在这里您可以选择返回哪些类名,例如根据配置。然而,当用户直接在Settings.yml
中实现您的插件或者通过手动实例化它时,您的PluginList类就不会被使用。这意味着,如果您的插件仅应直接从设置或在其他插件中手动使用,您就不需要PluginList类——这就是为什么它被标记为可选的原因。
示例插件
<?php namespace NamelessCoder\Gizzle\GizzlePlugins; use NamelessCoder\Gizzle\PluginInterface; use NamelessCoder\Gizzle\AbstractPlugin; /** * Example Gizzle Plugin * * Sends an email to the person who pushed the commit, * but only if the commit was made to the "demo" branch. */ class ExamplePlugin extends AbstractPlugin implements PluginInterface { /** * Returns TRUE to trigger this Plugin if branch * is "demo", as determined by REF of Payload. * * @param Payload $payload * @return boolean */ public function trigger(Payload $payload) { return 'refs/heads/demo' === $payload->getRef(); } /** * Send a thank you email to commit pusher. * * @param Payload $payload * @return void * @throws \RuntimeException */ public function process(Payload $payload) { $pusherEmail = $payload->getPusher()->getEmail(); $body = 'A big thank you from ' . $payload->getRepository()->getOwner()->getName(); $mailed = mail($pusherEmail, 'Thank you for contributing!', $body); if (FALSE === $mailed) { throw new \RuntimeException('Could not email the kind contributor at ' . $pusherEmail); } } }