用GitHub Actions构建Rails CI管道

188 阅读9分钟

GitHub Actions是一个自动化平台,你可以直接从 GitHub 仓库内运行。

使用GitHub Actions,你可以建立由任何类型的事件触发的工作流程。这些工作流程将任意代码作为Jobs ,你可以将多个Steps ,以实现你想要的几乎任何东西。

除了在每个拉动请求上自动发布GIF之外,这个新平台最明显的用例是建立一个测试CI/CD管道。

为什么要这么做?我已经在使用CircleCI / Travis / Semaphore...

好问题。所有这些CI/CD平台,或多或少都是等同的。

像CircleCI这样的专用CI服务在运行linters、执行测试、报告结果和启动部署等使用情况下是经过考验的。GitHub Actions更类似于乐高--它是一个用于运行任意工作流程和自动化的通用平台。

另一方面,GitHub Actions也包含在你的GitHub项目中。对于公共仓库,GitHub Actions 是免费的(截至本文撰写时没有限制)。对于私有仓库,每个仓库每月有2000分钟的免费构建时间--如果你有一个现有的GitHub公司计划,你将得到超过10,000分钟的构建时间,而且不需要额外费用。(完整的计费/使用限制在这里)

所有的竞争平台都有免费层级,而且,对项目来说,最大的好处是建立了任何一种CI/CD工具。供应商之间的差异是相当小的。

我正在努力使我的Rails应用程序尽可能地保持无聊。在构建产品和解决客户问题方面有足够的复杂性,我不需要用异国的运营设置为自己和团队创造更多的工作。

我还在一家定制软件咨询公司工作。我帮助其他公司运送产品和工具,所以少了一个需要处理的外部服务是很有吸引力的。

建立CircleCI并不难,但它多了一个需要注册的账户,多了一个放置公司信用卡的地方,多了一个我的客户可能会问**"嘿,这又是干什么的?"的东西。**

此外,我喜欢我们可以让CI管道靠近代码。在当前的CI/CD工具的浪潮中,最大的好处之一是向基础设施即代码的转变:你用一个平面文件来配置你的构建,并与你的项目一起提交。这比在维护不善的Jenkins实例中摸索要好得多,对管道的修改可以像代码库的其他部分一样被审查和合并。

GitHub Actions把这一点推到了极致。现在,你有一个单一的地方来处理项目的源代码、问题跟踪器、项目管理(GitHub Projects)、代码审查、安全警报,以及现在。CI/CD测试。一个账户,一个服务,一个地方。

从CircleCI迁移到GitHub Actions的普通Rails设置

如果你像大多数人一样,你可能在项目的第一周就根据thoughtbot的博文设置了CircleCI的配置,然后就再也没有碰过它。我也一样。

不过不用担心,从CircleCI 2.0的配置格式转移到GitHub Actions的语法是很简单的。

你的配置可能有点不同,但从概念上讲,一个Rails应用的虚拟CI/CD管道。

  • 检查出最新版本的代码
  • 设置一个包含Ruby、Node和一些浏览器测试工具的基础镜像
  • 设置一个PostgreSQL数据库服务
  • 安装依赖项(bundler,yarn,npm )并缓存结果,以便在不改变时加快构建速度
  • 设置测试数据库
  • 运行任何链接器/检查器 (rubocop,eslint,brakeman, 等等)
  • 运行测试

这些步骤按顺序运行,如果其中任何一个步骤失败,构建就会失败。你在GitHub的提交上得到一个红色的:x:,你不能合并你的PR,有人在Slack上对你大喊大叫,你知道这种情况。

为了在GitHub Actions上运行构建,我们只需将这些高级动作转换为匹配的工作流语法

我发现一个有用的变化是,利用GitHub Actions允许平行作业作为单一工作流的一部分来运行的优势。这个功能让我们可以把打结器和测试作为单独的作业来运行,这两个作业都必须通过,整个构建才能通过。

大多数竞争对手在免费/廉价层上没有提供太多的并行化。虽然每个作业都要设置基本环境,但我们可以通过同时运行最后的步骤来节省几分钟时间。(注意:这种方法将使用更少的挂钟时间,但你将使用更多的总构建时间)。

以下是我目前的 GitHub Actions 工作流程配置。

# Put this in the file: .github/workflows/verify.yml

name: Verify
on: [push]

jobs:
  linters:
    name: Linters
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup Ruby and install gems
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 10.13.0
      - name: Find yarn cache location
        id: yarn-cache
        run: echo "::set-output name=dir::$(yarn cache dir)"
      - name: JS package cache
        uses: actions/cache@v1
        with:
          path: ${{ steps.yarn-cache.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-
      - name: Install packages
        run: |
          yarn install --pure-lockfile

      - name: Run linters
        run: |
          bin/rubocop --parallel
          bin/stylelint
          bin/prettier
          bin/eslint
      - name: Run security checks
        run: |
          bin/bundler-audit --update
          bin/brakeman -q -w2

  tests:
    name: Tests
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:11
        env:
          POSTGRES_USER: myapp
          POSTGRES_DB: myapp_test
          POSTGRES_PASSWORD: ""
        ports: ["5432:5432"]

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup Ruby and install gems
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 10.13.0
      - name: Find yarn cache location
        id: yarn-cache
        run: echo "::set-output name=dir::$(yarn cache dir)"
      - name: JS package cache
        uses: actions/cache@v1
        with:
          path: ${{ steps.yarn-cache.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-
      - name: Install packages
        run: |
          yarn install --pure-lockfile

      - name: Setup test database
        env:
          RAILS_ENV: test
          PGHOST: localhost
          PGUSER: myapp
        run: |
          bin/rails db:setup

      - name: Run tests
        run: bin/rspec

一些注意事项。

  • 由于GitHub Actions是通用的自动化平台(而不仅仅是CI/CD),你需要告诉该动作何时运行:在这种情况下,我们希望它在每一个push 。如果你愿意,你可以进一步配置它,只在某些分支或文件上运行。

  • 在 GitHub Actions 中,我们建议你不要使用平台提供的 Docker 镜像和一堆常见的环境工具设置(例如circleci/ruby:2.6.3-node-browsers ),而是要组成其他第一方的 "设置 "动作,将二进制文件链接进来。在这种情况下,我们使用ruby/setup-rubyactions/setup-node 来包括我们想要使用的rubynode 的特定版本。这些步骤非常快,因为它们基本上是链接到预先建立的语言二进制文件。

  • 你可能注意到,我正在使用ruby/setup-ruby (一个社区行动),而不是第一方GitHubactions/setup-ruby 。当涉及到Ruby时,官方行动在功能和发布版本上都已经落后了。在GitHub行动的测试阶段,我们不得不完全停止使用行动,因为你无法轻易选择Ruby的具体次要发布版本,而官方行动在某些情况下,在支持最新的Ruby版本(包括安全版本)方面落后了几个月。ruby/setup-ruby 动作非常好用--你可以选择任何版本和任何口味(jruby,truffleruby, 等等),它将在5秒内拉出一个预构建的二进制文件。此外,ruby/setup-ruby 动作支持自动运行bundle install 并缓存你的宝石,而你不必手动操作cache 动作。

  • 许多设置PostgreSQL服务的文章(包括官方的例子)包括额外的健康检查选项,以确保数据库在继续进行之前已经启动。根据我的经验,等待健康检查的时间要多花15-60秒。由于我们在尝试连接到数据库之前必须安装宝石和进行其他设置,我删除了它们以减少运行时间,而且我没有遇到任何问题。至少可以考虑把例子中的健康间隔设置改成类似的。--health-interval 10ms --health-timeout 500ms --health-retries 15.这使我的 "初始化容器 "步骤从30秒减少到10秒,在每一个BUILD。

  • 实验一下alpine Postgres Docker镜像。这些镜像使用的是一个精简的Linux系统,比基本的Docker镜像要小。在我的测试中,alpine 镜像的下载和启动速度大约快33%。这样做的好处是,你可能无法运行与你的生产数据库完全相同的环境,但对于那些没有用Postgres做任何花哨事情的项目来说,对我来说,这似乎值得一试。要使用这些图像,在你的工作流程中用postgres:11-alpine 替换例如postgres:11 。你可以在Dockerhub上找到所有官方Postgres镜像的完整列表。我使用alpine 镜像已经运行了~18个月,没有遇到任何奇怪的问题。

  • 你应该检查你使用的各种工具,看看是否有选项可以加快CI环境下的运行时间。例如,yarn 有一个--pure-lockfile 选项,告诉它不要试图创建一个锁文件(因为你已经检查了一个......),rubocop 有一个--parallel 标志,以节省一点时间。当涉及到CI构建时,在这里和那里节省几秒钟很快就会增加。

总体印象

我花了一个晚上设置工作流程,目的是让它与我们现有的CircleCI设置的功能相同,我能够实现这个目标。

也就是说,我不是一个DevOps专家--我只是想建立一个坚实的管道,以便我可以专注于更重要的事情。

GitHub Actions是一个引人注目的选择,值得成为社区的默认选项。让GitHub成为你的仓库、问题跟踪器和构建服务器的一站式场所是非常有意义的。少了一个外部服务就意味着少了一些精神上的负担,一个更 "无聊 "的设置可以让我们专注于快速发货和解决客户问题,而不是做集成工作。

如果我开始一个新项目,我绝对会开始使用GitHub Actions作为我的默认CI服务。

2020年2月更新

本文已经更新:原稿是在GitHub Actions测试期间写的,许多需要改进的地方(即缓存)都得到了修复

2021年3月更新

本文已更新:改用ruby/setup-ruby动作的内置捆绑缓存选项