在我过去几年参与的所有项目中,有一个项目特别突出:我编写了一个名为 Great Eagle(托尔金的典故)的 WordPress 插件,它允许我的团队通过正常的 wp-admin 更新界面从我们的私有 Bitbucket 仓库安装和更新主题和插件。
这个插件使我们开发商店在开发最佳实践方面突飞猛进,其方式是我们从未预料到或预期的。它迫使我们使用正确的版本号,因为现在我们无法在没有版本号的情况下部署。它迫使我们将工作存储在 Bitbucket 中,因为现在我们无法在没有它的情况下部署。它迫使我们在部署工作之前使用命令行(我的意思是 git push origin master
),这随后导致我们使用 phpUnit。现在,除非我们的测试通过,否则我们无法部署。我们已经到达了测试驱动开发的涅槃,这一切都是因为我们从部署 git 的无关步骤开始的。
如果这一切听起来很标准且显而易见,那就太好了。我很乐意有机会向你学习。如果这听起来像是一些奇特的繁文缛节,那么猜猜看?这篇文章适合你。
免责声明:我在这款插件中的工作受到优秀的 GitHub Updater 插件(由 Andy Fragen 开发)的极大影响,在某些情况下甚至抄袭了该插件。我之所以编写自己的插件,是因为我们在 Bitbucket 中有数百个主题和插件,而且在我试用 GHU 时遇到了扩展性问题,这些问题后来已经得到解决。我可能过早地放弃了,因为该插件已经积极且专业地开发多年了。最重要的是,我们只是想要一个完全由我们自己维护的版本。我将展示一些来自我插件的代码片段,但最终**我建议用户使用 GHU**,因为它可能更适合大多数人,而且我不想从那个很棒的项目中获取任何动力。
先决条件
我的示例演示了多站点安装,但这并不特别重要。它在单站点安装上也能正常工作。我现在使用的是 WordPress 版本 4.8-alpha-39626,但这也不太重要。
最重要的假设是您工作场所中的所有主题和插件都存储在各自的 Bitbucket 仓库中。这是一个相当大的假设!说真的:当我们开始这项工作时,我们聘请了一家公司手动为我们每个主题和插件创建一个仓库。我们之前(糟糕地)使用的是 SVN,在迁移到 Bitbucket 之前。
它是如何工作的?
有三个(大约)步骤
1) 为用户创建一个 UI,以便他们可以进行 API 请求 到 Bitbucket,并将所有我们的仓库数据镜像到 WordPress 数据库中。实际上并不是所有仓库数据,只是代码段名称,我们将它用作更深入查询的键。

另一种方法是在该数据库为空时自动构建它,但就目前而言,我很乐意完全控制何时运行这样一个大型的 API 请求系列。
2) 一旦我们镜像了所有仓库的一些信息,我们就可以提供一个 jQuery 自动完成功能,用于选择几个仓库以进行数据深入挖掘,在该过程中我们会对每个仓库进行更多 API 调用,从而获得更深入的信息,例如版本号和下载 URL。

为什么不直接为所有仓库收集所有这些详细信息呢?因为我们有数百个仓库,而且每个仓库都需要进行多次调用才能获取所有相关信息,例如版本号。这可能需要 15 到 30 分钟,并进行 1000 多次 API 调用。
3) 一旦我们获得了有关目前想要使用的少量仓库的详细信息,我们就可以确定有关它们的两个重要事项。首先,它是否已安装在 WordPress 中?如果没有,它将出现在供我们安装的 UI 中。其次,如果它已安装,它是否是最新的版本?如果不是,它将出现在正常的 wp-admin 更新 UI 中。



如果仓库不可读(可能是它缺乏适当的文档块或命名约定),则会从所有这些步骤中省略它。这种情况只发生在我们少数命名不规范的插件上,但这很烦人,因为更改插件文件夹和文件名会停用该插件。
嗯。它到底是如何工作的?
这是一个合理的问题。我将解释哪些部分比较棘手,并分享一些来自我插件的代码。
构建仓库列表
每个 API 调用的最大仓库数量是 100。这就是 Bitbucket API 的工作方式。我们的帐户中的仓库数量远远超过 100 个,因此我们必须在循环中调用 Bitbucket
<?php
/**
* Store a "shallow" list of repos.
*/
public function set_repo_list() {
...
// Let's get 100 per page, which is the maximum.
$max_pagelen = 100;
....
// Get the first page of repos.
$page = 1;
$call = new LXB_GE_Call( 'api', "repositories/$team", $max_pagelen, $page );
$get = $call -> get();
$out = $get['values'];
// Now we know how many there are in total.
$total = $get['size'];
// How many pages does that make for?
$num_pages = ceil( $total / $max_pagelen );
// Query each subsequent page. We already got the first one.
while( $page < $num_pages ) {
$page++;
$next_call = new LXB_GE_Call( 'api', "repositories/$team", $max_pagelen, $page );
$next_get = $next_call -> get();
$next_repos = $next_get['values'];
$out = array_merge( $out, $next_repos );
}
// Sort the list by most recently updated.
$out = $this -> sort( $out, 'updated_on' );
$this -> repo_list = $out;
}
确定“主”插件文件
WordPress 在插件命名方面非常不固执己见。在大多数情况下,插件文件夹确实只包含一个插件,并且该插件将有一个“主”文件,该文件包含一个文档块,用于传达插件名称、描述、作者,最重要的是版本号。由于该文件可以命名为任何内容,因此确定哪个文件是主插件文件是一个开放性问题。我采取的方法是假设该插件将符合我们工作中尝试使用的某些命名约定。
<?php
function set_main_file_name() {
// Grab the slug name for this Bitbucket repo.
$slug = $this -> slug;
// Grab the list of file names in this repo.
$file_list = $this -> file_list;
// There's a good chance that there is a file with the same name as the repo.
if( in_array( "$slug.php", $file_list ) ) {
$main_file_name = "$slug.php";
// If not, there's a good chance there's a plugin.php file.
} elseif( in_array( 'plugin.php', $file_list ) ) {
$main_file_name = 'plugin.php';
// If not, it's probably a theme.
} elseif( in_array( 'style.css', $file_list ) && in_array( 'functions.php', $file_list ) ) {
$main_file_name = 'style.css';
// Else, oh well, couldn't find it.
} else {
$error = sprintf( esc_html__( 'Could not identify a main file for repo %s.', 'bucketpress' ), $slug );
$main_file_name = new BP_Error( __CLASS__, __FUNCTION__, __LINE__, func_get_args(), $error );
}
$this -> main_file_name = $main_file_name;
}
确定版本号
有了主插件或主题文件,我们就可以深入挖掘该文件中的文档块,以确定版本号。以下是我进行操作的方法
<?php
/**
* Get the value for a docblock line.
*
* @param string $key The key for a docblock line.
* @return string The value for a docblock line.
*/
function get_value_from_docblock( $key ) {
// Grab the contents of the main file.
$main_file_body = $this -> main_file_body;
// Break the file into lines.
$lines = $this -> formatting -> get_lines_from_string( $main_file_body );
// Let's save ourselves some looping and assume the docblock is < 30 lines.
$max_lines = 30;
$i = 0;
foreach( $lines as $line ) {
$i++;
// If the line does not have the key, skip it.
if( ! stristr( $line, $key . ':' ) ) { continue; }
// We found the key!
break;
// Whoops, we made it to the end without finding the key.
if( $i == $max_lines ) { return FALSE; }
}
// Break the line into the key/value pair.
$key_value_pair = explode( ':', $line );
// Remove the key from the line.
array_shift( $key_value_pair );
// Convert the value back into a string.
$out = implode( ':', $line_arr );
$out = trim( $out );
return $out;
}
顺便说一下,我要赞扬 php 的有用函数 version_compare()
,它可以解析大多数常见的版本语法
/**
* Determine if this asset needs to be updated.
*
* @return boolean Returns TRUE of the local version number
* is lower than the remote version number, else FALSE.
*/
function needs_update() {
$old_version = $this -> old_version;
$new_version = $this -> new_version;
$compare = version_compare( $old_version, $new_version );
if( $compare == -1 ) { return TRUE; }
return FALSE;
}
解析 readme.txt
我们实际上并没有在我们的插件中使用 readme.txt,因此我的 Great Eagle 插件也没有对它进行太多解析。但是,如果您希望合并 readme 信息,我建议使用 Ryan McCue 的这个库 来解析它。
私有仓库的处理方法
我们的仓库碰巧都是私有的,这只是我们目前开展业务的方式。为了查询它们,我们必须过滤一些凭据。在本例中,我通过基本身份验证进行过滤
<?php
/**
* Authenticate all of our calls to Bitbucket, so that we can access private repos.
*
* @param array $args The current args for http requests.
* @param string $url The url to which the current http request is going.
* @return array $args, filtered to include BB basic auth.
*/
public function authenticate_http( $args, $url ) {
// Find out the url to Bitbucket.
$call = new LXB_GE_Call( 'web', FALSE );
$bb_url = $call -> get_url();
// If we're not calling a Bitbucket download, don't bother.
if( ! stristr( $url, $bb_url ) ) { return $args; }
if( ! stristr( $url, '.zip' ) ) { return $args; }
// Okay, time to append basic auth to the args.
$creds = $this -> creds;
$args['headers']['Authorization'] = "Basic $creds";
return $args;
}
我通过过滤而不是将参数传递给 wp_remote_get()
来进行操作,因为我需要 WordPress 在它进行正常的主题和插件更新调用时为这些凭据做好准备,这些调用现在将转到 Bitbucket。
使用 OAuth 而不是基本身份验证会更好,但经过一番研究,我得出的结论是目前没有可行的方法。阻碍在于原始文件内容实际上不是 Bitbucket API 的一部分,它只是像任何其他静态资源一样托管在他们的网站上,例如这个 公共测试主题(它为了演示目的而公开,但同样,如果是私有的,你可以通过基本身份验证访问它)。我确实为我的努力准备了这个谦虚的 功能请求。作为一项安全措施,我建议使用 Bitbucket 的新 应用程序密码 功能专门为像这样的脚本调用创建一个帐户,并且该应用程序密码只有读取权限。因此,需要明确的是,使用基本身份验证,存在一个(可能就是这个)世界,在这个世界中,嗅探数据包的敌人正在读取我们的插件文件。我对此可以接受,至少现在可以。
将我们的仓库添加到更新队列
如果在这个整个过程中有一个关键点可以让我们站稳脚跟,那就是 wp_update_plugins() 函数。这是一个核心使用的巨大函数,它遍历所有已安装的插件,确定哪些插件有可用的更新,并将结果保存到一个瞬时变量。关键是这个瞬时变量随后被暴露出来以便进行过滤,这正是我插件所做的。
<?php
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'set_plugin_transient' ) );
/**
* Inject our updates into core's list of updates.
*
* @param array $transient The existing list of assets that need an update.
* @return The list of assets that need an update, filtered.
*/
public function set_plugin_transient( $transient ) {
if( ! is_array( $this -> assets_to_update ) ) { return $transient; }
foreach( $this -> assets_to_update as $asset ) {
if( empty( $asset -> transient_key ) ) { continue; }
if( ! $asset -> transient_content ) { continue; }
$transient -> response[ $asset -> transient_key ] = $asset -> transient_content;
}
return $transient;
}
我花了很长时间才突破这个障碍,而且我花了几个月的时间才写出这个插件。你可能应该直接使用 GHU。它与这个非常相似。也就是说,如果你想调整一些东西,并且你不喜欢运行第三方插件,那么上面的代码可能会帮助你编写自己的插件。
那么,确切地说,有什么意义呢?
重点不是如何构建自己的 Git 部署插件,或者应该使用哪个现有的插件。你可以自己弄清楚这些。真正有趣的是看看当我们开始从 Git 部署时发生了什么。一些副作用令人惊讶地积极。
再见,FTP
FTP 因为很多原因而很糟糕。
- FTP 访问是一个攻击媒介。
- 没有简单的方法来跟踪或恢复更改。
- 没有简单的方法允许多个人同时在同一个项目上工作。
- 人为错误。很容易误拖放,导致 WSOD 甚至更糟。
- 我从未预料到这一点,但在跨多个安装更新插件时,很明显,这种 Git 方法比 FTP 快得多。
使用像我在这篇文章中提倡和解释的 Git 部署系统,你可以完全禁用生产环境的所有 FTP 访问。说真的,你不需要它。
你好,正确的版本控制
我建议使用一个 Git 部署工具,该工具使用 docblocks 来确定版本号,并使用版本号来确定主题或插件是否需要更新。这将迫使你的团队使用正确的版本号,这是从仓促地开发主题到成熟地管理长期代码库的第一步。
我现在对单元测试非常兴奋
如果你没有进行单元测试,你可能知道你应该进行。使用 Git 部署,它可以是自动的,也是必需的。
我们使用命令行将我们的工作从本地 MAMP 移动到 Bitbucket,例如,git push origin master
。我们的每个插件都带有一个 Grunt 任务,在 git pre-commit 时执行我们的 phpUnit 测试,如果测试失败,提交也会失败。
我们使用 GitHooks 将 Grunt 绑定到我们的提交,并通过 Exec 执行我们的单元测试。如果测试失败,部署也会失败。
无法绕过测试,因为无法绕过 Git 进行部署!
回滚
这种方法没有真正的回滚。相反,你只能向前滚动。无论你想修复或恢复什么,都将其放入 master 分支,提高版本号,推送并部署。
人员配备
这种成熟可以产生全面的业务影响。想象一下:你有一些非开发人员支持人员在第一线,试图为客户调试问题。在过去,他们必须将这个请求放入开发票务队列中,而客户必须等待数小时或数天才能解决问题。现在不再需要这样了。现在,你的第一线支持人员可以导航到网络管理员,并看到在这个环境中,有问题的插件已过时。他们可以立即通过正常的 wp-admin 界面更新插件。票务由第一线支持人员解决,无需开发团队参与。也许这些第一线人员的成本低于开发人员,或者也许他们在账户管理方面拥有深厚的技能。无论哪种方式,你不再需要打开开发票务来部署内部插件的更新。至关重要。
机器崛起
在这个流程之前,我们非常像一个普通的开发工作室,为客户开发主题和插件,使用 FTP 进行操作,并且没有对我们的工作进行版本控制。为什么?因为我们很懒。为什么?因为我们是人。我们不再懒惰,因为我们不再是人,至少在部署时如此。我们是一系列命令行脚本和 API 请求,无论我们多么懒惰,我们都必须遵循正确的部署实践,因为我们已经取消了开发人员的 FTP 凭据!最重要的是,这是一种更快的部署方式,没有任何点击拖放的错误。
你能在一夜之间接受这个吗?好吧,不能。这是一个漫长而昂贵的过程,它可能不适合你,但说实话,它可能适合你。我认为大约有 1000 家开发工作室应该认真考虑一下。
你是否知道任何支持 AWS CodeCommit 的插件来完成这个任务?我们公司使用私有的 CodeCommit 仓库,我们一直在寻找部署自定义 WordPress 主题和插件的选项,就像你在文章中展示的那样。
如果你可以进行 API 调用来拼凑你的 CodeCommit 资源的版本号和 ZIP 链接,这是可以做到的。你将像我在文章中建议的那样过滤它们。
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'set_plugin_transient' ) );
Scott,
这是一篇写得很好且格式良好的文章,感谢你的时间和见解。但是,你可以免去编写插件和处理 API 的麻烦。我们开发了一个应用程序——Buddy.Works——它可以让你在将代码推送到 Bitbucket 仓库时自动执行所有这些操作。
在 Dev/Stage/Master 分支上开发插件模块
使用 Grunt 生成 CSS,压缩文件并运行单元测试
将插件和主题部署到生产服务器
监控客户端网站的停机时间并自动更新 WP/插件
使用 WP-CLI 创建备份
你完全正确,FTP 很糟糕,但你始终可以使用 SSH 密钥来保护 SFTP 部署和脚本执行。
以下是它的工作原理
我们还专门为部署 WordPress 主题提供了一份指南。
如果您能看一看并分享您的意见,我将不胜感激,尤其是您是一位经验丰富的 WP 开发人员。
哇,这太棒了!这正是行业需要的。你们似乎做得很出色。很惊讶我们之前没有发现这个。
我只对几个点提出一些小意见。
1) 我们喜欢通过正常的 wp-admin 界面进行更新,原因有几个。这是我们运行的所有 .org 插件更新的地方,因此我们可以集中在一个地方进行所有更新。此外,该界面对任何熟悉 WordPress 的人来说都很熟悉。
2) 关于您提到的“省去编写插件和处理 API 的麻烦”:我知道这取决于你是谁,但对我来说,这就像说,“省去深入而有价值的学习体验和成为这项有趣技术专家的麻烦”。有时,内部构建东西是一个让团队成员学习的宝贵机会!(但不总是)。
3) 如果我理解您对“项目”的定义,我们应该属于每月 300 美元的级别。我认为对于您提供的服务来说,这是一个很棒的价格,但根据查看的人的不同,它可能会引起一些争议。
这些只是一些我个人的微不足道的意见——你们的服务真的很棒。
对于更高级的 WordPress 工作流程,这是一种很棒的视角。感谢分享!
我很想了解更多关于您提到的测试和钩子的信息。我知道我应该使用测试,但我不知道从哪里开始。看到一个已建立的工作流程将有助于理解这个过程。
这可能超出了本文的范围。也许将来会讲到!
现在,对于测试,我强烈推荐 Pippin 的优秀教程
https://pippinsplugins.com/unit-tests-wordpress-plugins-introduction/
对于 GitHooks,请鼓起勇气,并考虑一下,这项工作是我第一次尝试使用 Grunt。您可以使用 Grunt,如果您能使用 Grunt,您就能做到这些事情。
干得好。为什么不把它作为每个插件的库?这将为您省去设置菜单,我认为这对用户来说更好,因为我们不会将开发插件设置为可见……
嗨,大卫!感谢您的阅读和评论。
我不太理解你的问题。
是的,我的意思是它作为插件内部的库会更好吗?