使用 GitHub Actions 构建管道

在了解了 CI 的所有阶段之后,是时候进行实践了。向你介绍一个或多个 CI/CD 工具的所有功能超出了本书的范围;不过,我们仍然希望向你展示建立一个工作构建管道是多么容易。为了尽可能降低入门门槛并避免任何成本,我们决定使用 GitHub Actions

GitHub Actions 并非像 Jenkins 或 CircleCI 那样的经典 CI 工具,而是一种围绕 GitHub 资源库构建工作流的方法。只要有一点创造力,你就能做得比 "仅仅 "一个经典的 CI/ CD 管道要多得多。当然,我们将只关注这一方面。

你可能已经有了一个 GitHub 账户,如果还没有,申请一个也不会花你什么钱。在撰写本文时,你可以每月免费使用 GitHub Actions 多达 2,000 分钟,这让它成为你开源项目的绝佳游乐场或有用工具。

示例项目

我们创建了一个小型演示应用程序,供本章使用。你可以在这里找到它: https://github.com/PacktPublishing/Clean-Code-in-PHP/tree/main/ch11/example-application 。请注意,除了演示 GitHub Actions 和 Git 钩子的基本使用方法外,它没有任何其它用途。

GitHub Actions 简而言之

GitHub Actions 不提供花哨的用户界面来配置所有阶段。相反,一切都通过直接存储在版本库中的 YAML Ain’t Markup Language(YAML) 文件进行配置。作为一名 PHP 开发人员,你很可能有使用 YAML 文件进行各种配置的经验—​如果没有,也不用担心,因为它们很容易理解和使用。

GitHub 的操作是围绕工作流组织的。工作流由特定事件触发,包含一个或多个在事件发生时要执行的作业。一个作业由一个或多个步骤组成,每个步骤执行一个动作。

这些文件必须存储在资源库的 .github/workflows 文件夹中。让我们看看 ci.yml 文件的第一行,这将是我们的 CI 工作流程:

name: Continuous Integration
on:
    workflow_dispatch:
    push:
        branches:
          - main
    pull_request:
jobs:
    pipeline:
        runs-on: ubuntu-latest
        steps:
            - name: ...
              uses: ...

这已经是相当多的信息了。让我们逐行浏览一下:

  • name 定义了工作流在 GitHub 中的标记方式,可以是任何字符串

  • on 说明了哪些事件应触发该工作流;这些事件包括以下内容:

    • workflow_dispatch 允许我们从 GitHub 网站手动触发工作流,这对于创建和测试工作流来说非常有用。否则,我们就需要每次都推送提交到 main 或创建 PR。

    • push 告诉 GitHub,每当有推送发生时,就执行这个工作流。我们将范围缩小到只推送主分支。

    • 此外,pull_request 还会在每次新建 PR 时触发工作流。配置看起来可能有点不完整,因为冒号后面没有更多信息。

  • jobs 包含要为此工作流程执行的作业列表,详细信息如下:

    • pipeline 是此 YAML 中唯一作业的标识符 (ID)。我们选择 pipeline 这个词是为了说明我们可以使用 GitHub Actions 来构建我们的 CI/CD 管道。请注意,ID 必须由一个单词或多个单词组成,并由下划线 (_) 或破折号 (-) 连接。

    • runs-on 命令 GitHub 使用最新的 Ubuntu 版本作为运行程序(即平台)。其它可用的平台有 Windows 和 macOS。

    • steps 标记该作业要执行的步骤列表。下一节我们将对此进行详细介绍。

现在我们已经配置了工作流程的基本要素,可以开始添加构建阶段了。

第 1 阶段:构建项目

这些步骤正是 GitHub Actions 的强大之处:在这里,你可以从大量已有的操作中进行选择,以用于你的工作流程。这些操作都在 GitHub Marketplace 中( https://github.com/marketplace )。让我们在工作流 YAML 中添加一些步骤,如下所示:

steps:
    ###################
    # Stage 1 - Build #
    ###################
    - name: Checkout latest revision
      uses: actions/checkout@v3
    - name: Install PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
        coverage: pcov

GitHub 维护的操作可以在 actions 命名空间中找到。在我们的示例中,它是 actions/checkout,用于签出版本库。我们暂时不需要指定任何参数,因为该操作会自动使用该工作流文件所在的版本库。

@V3 注解用于指定要使用的主要版本。对于操作/结账,这将是版本 3。请注意,使用的始终是最新的次要版本,在撰写本文时为 3.0.2 版。

另一个操作是 shivammathur/setup-php,它是由许多将自己的工作成果作为开放源代码提供的优秀人员之一提供的。在这一步中,我们使用 with 关键字指定更多参数。在本例中,我们使用 php-version 选项在之前选定的 Ubuntu 机器上安装 PHP 8.1。使用 coverage 参数,我们可以告诉 setup-php 启用 pcov 扩展来生成代码覆盖率报告。

动作参数

前面介绍的两个操作所提供的参数都远远超出了我们在此所能描述的范围。你可以在 Marketplace 中查找有关其功能的更多信息。

关于格式,我们在步骤之间使用了注释和空行,以使文件更易于阅读。没有约定俗成的格式,以后如何格式化 YAML 文件完全取决于你自己。

下一步是安装项目依赖项。对于 PHP,这通常意味着运行 composer install。请注意,我们不使用 --no-dev 选项,因为我们需要安装开发依赖项来执行所有质量检查。我们将在管道结束时再次删除它们。

依赖关系管理

在本章中,我们以 Composer 工作流程为例管理代码质量工具,因为这是最常用的方法。不过,我们在第 9 章 "组织 PHP 质量工具" 中介绍的其它两种组织代码质量工具的方式,也同样适用于 GitHub Actions。在那一章中,我们还详细解释了 --no-dev 选项。

接下来的步骤可能如下所示:

- name: Get composer cache directory
  id: composer-cache
  run: echo "::set-output name=dir::$(composer config cache files-dir)"
- name: Cache dependencies
  uses: actions/cache@v2
  with:
    path: ${{ steps.composer-cache.outputs.dir }}
    key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
    restore-keys: ${{ runner.os }}-composer-
- name: Install composer dependencies
  run: composer install

要实现 Composer 依赖项的缓存,GitHub 操作需要一些手动工作。第一步,我们将使用 config cache-files-dir 命令从 Composer 获取的 Composer 缓存目录位置存储在名为 dir 的输出变量中。注意这里的 id:composer-cache—​我们将需要它在下一步中引用该变量。

然后,我们在下一步中通过使用 steps.composer-cache.outputs.dir 引用(上一步中设置的 id 值和变量名的组合)来访问该变量,以定义 actions/cache 操作应缓存的目录。 key 和 restorekey 用于生成唯一的缓存密钥,即存储 Composer 依赖项的缓存条目。

最后,我们使用 run 参数直接执行 composer install,就像在 Ubuntu 机器上本地执行一样。需要注意的是:你可以,但不必在每一步都使用现有的 GitHub 操作,你也可以直接执行纯 shell 命令(或 Windows 运行环境下的相应命令)。

Marketplace 中也有一些操作可以接管命令的编写工作,例如 php-actions/composer。我们在这里没有首选的解决方案,两种方案都可以。

因为我们要在示例应用程序的 API 上运行集成测试,所以需要运行网络服务器。对于我们的简单用例,使用 PHP 内置的网络服务器就足够了,我们可以在下一步开始使用:

- name: Start PHP built-in webserver
  run: php -S localhost:8000 -t public &

-S 选项告诉 PHP 二进制文件启动一个在 localhost 地址和 8000 端口监听的网络服务器。由于我们从项目的根目录启动,因此需要使用 -t 选项定义文档根目录(网络服务器查找要执行的文件的文件夹)。在这里,我们要使用公共文件夹,其中只包含 index.php 文件。最好不要在文档根文件夹中存储任何其它代码,因为这样攻击者就很难入侵我们的应用程序。

PHP built-in web server

请注意,PHP 的内置网络服务器只能用于开发目的。绝不能用于生产,因为它在构建时并未考虑性能或安全性。

你肯定注意到了命令末尾的 "&"。它告诉 Linux 执行命令,而不是等待命令终止。如果没有它,我们的工作流程就会卡在这一点上,因为网络服务器不会自行终止,因为它需要继续监听请求,直到我们在稍后阶段运行 集成测试

构建环境的设置已经完成。现在,是时候对我们的示例应用程序运行第一次代码质量检查了。

第 2 阶段:代码分析

在第一个构建阶段,我们创建了构建环境并检查了应用程序代码。此时,应用程序应该是完全正常的,可以随时进行测试。现在,我们要做一些静态代码分析。

标准方法是为每个工具使用专用的 GitHub 操作。这样做的好处是,我们可以让开发工具远离构建环境,因为它们将在单独的 Docker 容器中执行,用完后会立即丢弃。不过,这种方法也有一些缺点。

首先,每次操作都会引入另一个依赖关系,我们需要依赖作者保持更新,并且在一段时间后不会失去维护它的兴趣。此外,我们还增加了一些开销,因为 Docker 映像通常比实际工具大很多倍。最后,当我们的应用程序设置变得更加复杂时,在单独的 Docker 容器中运行代码质量工具可能会导致问题,原因很简单,因为它与构建环境不同。有时,一些微小的差异也会导致问题的出现,让你花费数小时或数天来解决这些问题。

正如我们在上一节中看到的,我们可以在构建环境中简单地执行 Linux shell 命令,因此没有什么可以反对直接在构建环境中执行代码质量工具,我们只需要确保事后删除它们,以免它们被发布到生产环境中。

在我们的示例应用程序中,我们将 PHP-CS-Fixer 和 PHPStan 添加到了 composer.json 文件的 require-dev 部分。在工作流程 YAML 中添加以下几行代码,就可以让它们作为下一步执行:

###########################
# Stage 2 - Code Analysis #
###########################
- name: Code Style Fixer
  run: vendor/bin/php-cs-fixer fix --dry-run

- name: Static Code Analysis
  run: vendor/bin/phpstan

这里我们不需要太多参数或选项,因为我们的示例应用程序提供了 .php-cs-fixer.dist.phpphpstan.neon 配置文件,这两个工具都会默认查找这些文件。只有 PHP-CS-Fixer 会使用 --dry-run 选项,因为我们只想在 CI/CD 管道中检查问题,而不是解决问题。

设置检查范围

对于我们的小型示例应用程序,在所有文件上运行前面的检查是没有问题的,因为它们会很快执行。但是,如果我们的应用程序不断扩大,或者我们希望在现有应用程序中引入 CI/CD(我们将在本章中进一步讨论),那么只需在最新提交中发生变化的文件上运行这些检查即可。在这种情况下,以下操作可能会对你有所帮助: https://github.com/marketplace/actions/changed-files

如果 PHP-CS-Fixer 和 PHPStan 都没有报告任何问题,我们就可以放心地执行下一阶段的自动测试:测试。

第 3 阶段:测试

我们已经彻底分析和检查了代码中的错误和语法错误,但我们还需要检查代码中的逻辑错误。幸运的是,我们有一些自动测试来确保我们没有在无意中引入任何错误。

出于与第二阶段代码质量工具相同的原因,我们不想使用专门的操作来运行 PHPUnit 测试套件。我们只需像在本地开发系统上一样执行 PHPUnit。在这里,使用 phpunit.xml 文件显然很有用,因为我们不需要记住这里的所有选项。让我们先看看工作流程 YAML,如下所示:

###################
# Stage 3 - Tests #
###################
- name: Unit Tests
  run: vendor/bin/phpunit --testsuite Unit
- name: Integration Tests
  run: vendor/bin/phpunit --testsuite Api

这里唯一值得注意的是,我们并不只是运行所有测试,而是将它们分成两个测试套件:单元测试和 Api 测试。由于单元测试的执行速度最快,我们希望先运行它们(并导致失败),然后再运行速度较慢的集成测试。请注意,我们没有添加任何 E2E 测试,因为我们的应用程序不是在浏览器中运行,而只是一个网络服务。

我们使用 phpunit.xml 配置文件来分割测试。下面的代码片段显示了 <testsuites> 节点,我们在这里按目录(Api 和 Unit)将套件分开:

<testsuites>
    <testsuite name="Api">
        <directory>tests/Api</directory>
    </testsuite>
    <testsuite name="Unit">
        <directory>tests/Unit</directory>
    </testsuite>
</testsuites>

我们还配置了 PHPUnit 以创建代码覆盖率报告,如图所示:

<coverage processUncoveredFiles="false">
    <include>
        <directory suffix=".php">src</directory>
    </include>
    <report>
        <html outputDirectory="reports/coverage" />
        <text outputFile="reports/coverage.txt" />
    </report>
</coverage>

要创建这些报告,PHPUnit 会自动使用 pcov 扩展名,我们在第一阶段配置了该扩展名。它们将被写入 reports 文件夹,我们将在下一阶段进行处理。

这就是测试阶段需要完成的所有工作。如果测试没有发现任何错误,我们就可以进入管道的最后一个阶段,结束一切工作。

第 4 阶段:部署

现在,我们已经对应用程序进行了全面的检查和测试。在准备将其部署到我们所设想的环境中之前,我们需要先移除开发依赖项。幸运的是,这非常容易,我们可以在这里看到:

####################
# Stage 4 - Deploy #
####################
- name: Remove dev dependencies
  run: composer install --no-dev --optimize-autoloader

运行 composer install --no-dev 只需删除 vendor 文件夹中的所有 dev 依赖项即可。另一个值得注意的功能是 Composer 的 --optimize-autoloader 选项:因为在生产环境中,我们不会像在开发环境中那样添加或更改任何类或命名空间,所以 Composer 自动加载器可以通过不检查任何更改来进行优化,从而加快磁盘访问速度。

最后一步,我们要创建构建工件:一个工件是交付成果,即我们打算部署的代码。另一个工件是我们在第 3 阶段创建的代码覆盖率报告。工作流 YAML 执行完毕后,除了显示在 GitHub 网站上的日志信息外,GitHub Actions 不会保留任何其它数据,因此我们需要确保在最后将它们存储起来。下面的代码片段对代码进行了说明:

- name: Create release artifact
  uses: actions/upload-artifact@v2
  with:
    name: release
    path: |
      public/
      src/
      vendor/
- name: Create reports artifact
  uses: actions/upload-artifact@v2
  with:
    name: reports
    path: reports/

我们使用 actions/upload-artifacts 操作来创建两个 ZIP 压缩文件(此处称为工件):版本和报告。第一个压缩包包含在生产环境中运行应用程序所需的所有文件和目录,仅此而已。我们省略了项目根目录下的所有配置文件,甚至包括 composer.json 和 composer.lock 文件。我们不再需要它们,因为我们的供应商文件夹已经存在。

报告工件将只包含报告文件夹。构建完成后,你只需在 GitHub 上分别下载这两个 ZIP 压缩包。更多信息请见下一节。

将管道集成到你的工作流程中

将工作流 YAML 添加到 .github/workflows 文件夹(例如 .github/workflows/ci.yml)后,只需提交并推送到版本库即可。我们将管道配置为在每次打开 PR 或有人向主分支推送提交时运行。

打开 https://github.com 进入版本库页面后,你会在 Actions 选项卡上看到最近一次工作流运行的概览,如下截图所示:

image 2023 11 12 17 07 45 484
Figure 1. Figure 11.3: The repository page on github.com

绿色对勾表示运行成功,红色叉表示运行失败。你还可以看到它们的执行时间和耗时。点击每个条目右侧的三个点,可以找到更多选项—​例如,可以删除工作流运行。点击运行的标题,也就是运行对应的提交信息,就会进入 Summary 页面,如下图所示:

image 2023 11 12 17 08 37 196
Figure 2. Figure 11.4: The workflow run summary page

在这里,你可以看到工作流程中的所有任务。由于我们的示例只包含一个作业(管道),因此你只能看到一个。在这一页,你还可以找到任何生成的工件(比如我们的发布和报告工件),并下载或删除它们。GitHub 只提供有限的免费磁盘空间,所以请确保在空间用完时删除它们。

另一个重要信息是计费时间。虽然我们的工作总共只运行了 43 秒,但 GitHub 会从你的月使用量中扣除 1 分钟。虽然 GitHub 提供的免费计划很慷慨,但你还是应该时不时看看自己的使用情况。你可以在用户设置页面的 Billing and plans 部分( https://github.com/settings/billing )找到更多相关信息。

如果想查看工作流运行过程中发生的具体情况—​例如,如果出了问题,可以点击管道作业,查看所有步骤的详细概览,如下面的截图所示:

image 2023 11 12 17 10 00 089
Figure 3. Figure 11.5: Job details page

每个步骤都可以展开或折叠,以获取有关执行过程中发生的具体情况的更多信息。在前面的截图中,我们展开了安装 PHP 步骤,以查看该操作的详细信息。

恭喜你,现在你的项目已经有了一个可用的 CI 管道!我们的 GitHub 操作之旅到此结束。当然,你还可以随意扩展管道—​例如,将发布工件上传到 SSH 文件传输协议(SFTP)服务器或 AWS 简单存储服务(S3)桶。可以做的事情还有很多,所以一定要多加尝试。

在下一节中,我们将向你展示如何设置本地管道。这将通过早期检查避免不必要的工作流运行,从而为你节省一些时间,甚至可能节省成本。