构建管道
在上一节中,我们列举了许多必要的步骤,以使我们的代码可以随时运往生产环境。在 CI 中,这些步骤的组合就是我们所说的构建管道:它接收输入(在我们的例子中,就是所有的应用程序代码),通过多个工具运行,并从中创建所谓的构建工件。它们是构建的结果—通常包括可交付成果(可移动到所需环境的应用程序代码包),以及额外的数据,如构建日志、报告等。
下图为典型的构建流水线示意图。由于不在本地环境中执行,因此需要两个额外步骤:创建构建环境 和 构建应用程序:

在接下来的章节中,我们将详细介绍每个构建阶段。一开始我们将保持理论性,然后在本章后面部分给出技术实现的例子。
第 1 阶段:构建项目
CI 管道要求为我们的应用程序创建一个专用的构建实例,在该实例中,我们可以隔离运行所有工具和检查。这大致可分为两个步骤:创建构建环境和运行必要的构建工具。
创建构建环境
要在本地开发系统之外的其它地方构建应用程序,我们首先需要创建一个构建环境。具体如何提供环境取决于所使用的 CI/CD 工具。它可以是一个为每个项目提供独立工作空间的专用服务器,也可以是一个完全容器化的 Docker 环境,每次需要时都会启动,而且只在构建期间持续运行。
一旦有了构建环境,我们就需要在那里下载所有源代码,但暂时不需要外部软件包或其它依赖项。你的代码很可能存储在 Git 仓库中,并托管在私人 Git 服务器或商业服务上。下载版本库特定分支的副本称为签出(checkout)。
我们必须注意代码是从仓库的哪个分支签出的。这取决于你想构建什么。如果你想检查一个拉取请求(PR)(对代码的一系列修改,有人要求将其集成到代码库中)的代码,那么你需要签出该功能的分支。如果要将应用程序的最新版本上传到生产环境,则需要查看主分支。
不过,如果你的项目没有使用 Git 托管,也不用担心,这一步仍然是必要的,因为我们需要将代码放到 CI/CD 服务器上。无论是通过 Git、Mercurial、Subversion (SVN),还是直接下载文件,最终都没有关系。这一步的结果是将我们要部署的代码随时放在 CI/CD 服务器上,这样我们就可以在下一步开始安装依赖项。
构建应用
构建应用程序与在新系统上安装类似。在上一步中,我们确保源代码在环境中可用。在这一步中,我们需要执行任何必要的构建步骤。这通常包括以下步骤:
-
安装外部依赖项:你的版本库应只包含你自己的代码,不包含外部依赖项。例如,我们可以通过 Composer 或 PHAR 安装与验证环境 (Phive) 来管理这些依赖项。
-
创建配置文件:你的版本库不应包含任何密码或其它机密数据,如应用编程接口 (API) 密钥。它们可以安全地存储在 CI/CD 工具中,并在此阶段用于创建环境文件(例如 .env)。
-
准备测试数据库:要运行集成测试或 E2E 测试,构建实例需要一个可用的数据库。通常的做法是创建测试数据库、导入数据库模式、运行其它数据库迁移,最后在数据库中填充测试数据。
为了缩短构建时间,许多现代 CI/CD 工具都提供缓存功能。如果激活,它们会在第一次下载后将依赖项保存在临时存储中。如果默认情况下未激活缓存,通常最好将其打开。
第 2 阶段:代码分析
我们在第 7 章 "代码质量工具" 中详细介绍了代码质量工具。现在,是时候将这些工具添加到我们的管道中了,以确保每次引入变更时都能执行这些工具。
PHP linter
如果你把代码合并到另一个分支,代码总是有可能被破坏。虽然 Git 有非常精密的合并算法,但它也不能变魔术。如果你有一个高覆盖率的测试套件,如果合并导致了语法错误,某些测试肯定会被破坏。那么,我们为什么要运行这个额外的步骤呢?我们建议这样做,因为 PHP 连接器有两个优点:运行速度非常快,而且会检查所有的 PHP 文件,不管这些文件是否有测试。
如果检测到任何问题,我们希望我们的流水线能快速失败。因此,在执行任何长时间运行的任务之前,最好先运行一次快速语法检查。在任何情况下,它们都会中断,而你也会失去一些宝贵的时间。根据经验,检查运行得越快,它就会越早出现在管道中。
代码样式检查
检查完代码语法后,就该检查代码样式了。这项操作速度也很快,因此在流程的早期运行它是合理的。在本例中,我们将使用 PHP Coding Standards Fixer (PHP-CS-Fixer),我们在第 7 章 "代码质量工具" 中已经介绍过它。
在本地运行 PHP-CS-Fixer 和在 CI/ CD 管道中运行 PHP-CS-Fixer 有一个微妙但重要的区别:对于后者,我们只用它来检查代码,而不是修复代码。我们不希望管道更改我们的代码,而只是对其进行分析。换句话说,我们的管道只会检查代码的格式是否正确(根据我们定义的规则),但不会试图自动修复;如果违反了任何规则,构建就会失败。
没有任何规则规定 CI/CD 管道不应更改代码。但是,在过程中自动将更改提交到版本库会增加复杂性。此外,这还需要一个经过很好测试的应用程序,你需要相信你选择的工具不会破坏任何东西。通常情况下,它们都能很好地工作,但你想冒这个险吗?
在本地环境中,在运行代码样式检查程序的同时运行修复程序是合理的。我们将在本章的下一节讨论本地设置。
静态代码分析
至此,我们知道我们的代码在语法上是正确的,格式也符合我们的规则。前面的两项检查通常都能快速完成,因此如果出现了这些容易发现的问题,我们的构建就会提前失败。
现在,是运行较慢任务的时候了。静态代码分析所需的时间通常比前两项要长一些,但远没有运行自动测试来得慢。从本质上讲,这一步与 "inting "和 "代码风格检查 "并无太大区别:如果违反了我们之前定义的规则,构建就会失败。
如果要在现有项目中引入 CI,面临的挑战就是如何找到错误报告的最佳位置。一方面,你要让开发人员满意,而不是强迫他们修复其它开发人员在他们接触的每个文件上引入的几十个问题。另一方面,你需要设置足够严格的阈值,以便在每次代码变更时至少强制执行一些重构。
不幸的是,这里没有金科玉律,你需要对设置进行试验。稍后,当静态代码分析报告的大部分问题都得到解决时,你需要稍微收紧错误报告规则,这样你的项目才不会停滞在某个水平上。
第 3 阶段:测试
一旦我们的代码到达管道中的这一步,我们就可以确信它在语法上是正确的,符合我们的代码样式指导原则,并且根据我们的静态代码分析规则没有任何一般缺陷。现在,我们将运行流程中通常耗时最长的步骤:自动测试。
正如我们在上一章所介绍的,需要考虑的不仅仅是单元测试。通常情况下,像网络服务这样的项目至少会有一些集成测试,以确保服务整体运行良好,包括数据库事务。或者,如果你的项目是一个传统的网络应用程序,你可能会有一个 E2E 测试套件,利用浏览器来虚拟点击通过它。
在这里,我们要采用与构建步骤相同的方法:从快速运行的测试开始,然后继续慢速测试。如果单元测试已经失败,就无需等待 E2E 测试的结果。因此,测试的执行顺序通常是这样的:
-
单元测试
-
集成测试
-
E2E 测试
无论哪种类型的测试,只要有一个失败,构建就会被标记为失败。如果所有测试都通过了,我们就已经通过了管道中最关键的部分。我们的应用程序已经可以部署了,现在是清理和准备交付的时候了。
第 4 阶段:部署
我们的代码已经过各种工具的全面检查,我们确信它符合我们的标准。现在我们可以准备 构建工件,并最终部署应用程序。让我们看看为此需要做些什么。
收集数据
在前几个阶段,我们使用的所有工具都会产生某种数据,无论是通过写入标准输出(stdout),还是通过创建报告来总结所执行的操作。
例如,你可以将生成的报告上传到专用存储空间或推送到你的存储库。或者,也可以使用 PHPUnit 的代码覆盖率报告,从中自动创建代码覆盖率徽章,并将其添加到 GitHub 项目的 README 中。
不过,最重要的用例是调试是否有任何阶段失败。调试输出永远都不够用,以防出错,因此最好将工具的冗余度设置得更高一些。你的 CI/CD 工具通常会确保在构建管道执行完毕后,所有写入 stdout 的内容都会可用。
清理
在将应用程序上传到其它地方之前,我们要确保它不包含任何不必要的 "压舱物"。这包括删除前一阶段的日志或报告,或删除代码质量工具。请记住,我们只应将应用程序运行所需的代码部署到生产环境中—PHPUnit 等开发工具在构建时并没有考虑到安全性( https://phpunit.readthedocs.io/en/9.5/installation.html#webserver )。
部署
要将代码部署到目标环境中,我们需要将其封装成一个资产,以便于移动到目标环境中。这种资产也称为交付品。我们为可交付成果选择的类型取决于应用程序部署到生产环境的方式。可交付成果的常见类型是必须部署的代码的简单归档。
例如,如果你的生产环境运行在传统的内部网络服务器上,我们就需要创建应用程序代码存档,将其上传到目标服务器,然后从那里提取出来。
如今的事实标准是使用 Docker 的容器化环境。一旦构建实例经过全面测试,就可以从中创建一个 Docker 镜像。然后,该镜像将被上传到一个镜像存储库,如亚马逊网络服务弹性容器存储库(AWS ECR)。这样一个镜像存储库可以存放所有镜像,因此在需要时可以用来启动新的容器。这种方法为我们今天所看到的高度可扩展的网络应用程序铺平了道路,因此,如果你的应用程序在某些时候需要扩展,那么从一开始就将你的应用程序设计成可使用 Docker 的,这将会给你带来回报。
现在我们对构建管道的样子有了一个很好的概念。在开始设置我们的示例流水线之前,我们还需要弄清楚一件事—什么时候执行?
将管道集成到你的工作流程中
设置完所有必要步骤后,我们最终需要将管道集成到工作流程中。CI/ CD 工具通常会就管道的执行时间提供不同的选项。一开始,当然可以通过点击按钮手动执行。不过这样做并不方便。如果你使用托管的 Git 仓库,如 GitHub、GitLab 或 Bitbucket,你可以将它们与构建管道连接起来,每当有 PR 创建或有分支合并到主分支时就开始构建。
对于需要花费数小时进行构建的大型项目,在夜间运行当前代码库的构建(即所谓的夜间构建)也很常见。开发人员第二天就能从管道中获得反馈。
运行构建需要一些时间,当然,开发人员不应该坐在屏幕前等待,直到他们可以继续工作为止。他们应该在构建成功或失败后立即得到通知。如今,所有的 CI/CD 工具都提供了多种通知开发人员的方式,主要是通过电子邮件以及 Slack 或 Microsoft Teams 等聊天工具中的消息。此外,这些工具通常还提供仪表盘视图,你可以在一个屏幕上看到所有构建的状态。
现在,你应该对项目的构建管道有了一个很好的概念。因此,现在是向你展示一个实际例子的时候了。