如何在Ruby CI管道中利用静态代码分析(详细教程)

153 阅读5分钟

持续集成,或CI,指的是能够持续将功能和错误修正合并到代码库主分支的文化和技术。代码修改在测试后立即被纳入,而不是在瀑布式发布过程中与其他更新捆绑在一起。

同样,持续交付,或称CD,指的是自动将修改后的代码部署到目标环境中,例如将生产前的分支部署到暂存区,将主分支部署到生产区。CD接续CI的工作,因此,它们往往是携手并进的。

静态代码分析通常属于CI/CD管道中的CI方面。以一个小型的Ruby项目为例,我们将建立一个CI工作流程,在以下领域使用静态分析来分析代码质量:

  • 一致性,与广泛采用的Ruby风格指南的一致性。
  • 布局,如不公正的间距或错位的缩进。
  • 提示,如不适当的权限或多余的操作。
  • 安全分析,如不安全方法的使用。

前提条件

创建一个沙盒

让我们为今天的冒险制作一个全新的目录。在该目录下初始化一个 Git 仓库,并将其检入 GitHub,因为我们将使用 GitHub 工作流作为我们的 CI 工具(稍后会有更多介绍):

$ mkdir proj
$ cd proj/
$ touch .gitignore

设置Ruby和Bundler

你可能已经在你的电脑上安装了Ruby。但我发现最好不要使用预装的Ruby,原因有二。第一个原因,它通常比最新的稳定版本要老得多,而你不想错过Ruby的最新功能,不是吗?原因之二,通过安装、删除或更新一个关键的软件包,很容易破坏你的系统。

不要着急,有一个解决方案--RVM。我不会在这里讨论RVM的细节,但使用它,你可以在一个系统上安装和管理多个版本的Ruby,保持系统Ruby的原始状态:

$ rvm install 3.0.0
$ rvm use 3.0.0

接下来,我们设置了Bundler,一个用于Ruby的神奇的包管理器。我们需要它来跟踪我们项目的依赖性。它的安装非常简单明了:

$ gem install bundler
$ bundle init

为了确保我们的项目宝石保持本地化,我们可以设置Bundler将宝石安装在指定的路径。创建一个目录.bundle/ ,并在该目录下创建一个config 文件,内容如下。

通过这种配置,Bundler将把所有宝石安装在当前项目文件夹proj/ 内的一个.gems/ 文件夹内。将这两个目录.bundle/.gems/ 添加到你的.gitignore 文件中,这样它们就不会被检入VCS。

熟悉Rubocop

为了分析我们上面提到的所有领域的代码质量,我们将使用Rubocop,这是Ruby中最好的译码器之一。Rubocop有一个广泛的规则集合,被称为 "警察",根据其功能被组织在一组,称为 "部门"。

要安装,在你的Gemfile中添加这一行,然后运行bundle install

  # frozen_string_literal: true

  source 'https://rubygems.org'

  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

+ gem 'rubocop', '~> 1.9', require: false

要列出任何给定文件或目录中的所有罪行,只需将名称作为参数传给rubocop

$ rubocop 

Rubocop还能够自动纠正它所报告的大部分错误,这对它来说是非常有用的。要启用自动更正功能,请传递-a 标志。传递-A 会使用更积极的自动更正模式,除非你确定你在做什么,否则不建议这样做:

$ rubocop -a  # safe autocorrect, recommended
$ rubocop -A  # unsafe autocorrect, not recommended

如果Rubocop不是全局安装的,你将需要在这些命令前加上bundle exec

代码

现在我们进入了有趣的部分,在Ruby中编写脚本。以这个脚本为例。它将一个文件名作为参数,并将该文件的内容打印到STDOUT,与cat 命令非常相似(因此而得名):

# cat.rb
filename = ARGV[0]

file = open(filename)
list = file.read
file.close

list.each_line.with_index{|line|
  puts line
}

它的格式很差,违反了风格指南中的许多规则,甚至还有一些明显的安全漏洞。我们很快就会解决这些问题,但首先,让我们用Rubocop做一次初步扫描,并观察输出结果:

$ bundle exec rubocop cat.rb
Inspecting 1 file
W

Offenses:

cat.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
# cat.rb
^
cat.rb:4:8: C: Security/Open: The use of Kernel#open is a serious security risk.
file = open(filename)
       ^^^^
cat.rb:8:16: W: [Correctable] Lint/RedundantWithIndex: Remove redundant with_index.
list.each_line.with_index{|line|
               ^^^^^^^^^^
cat.rb:8:26: C: [Correctable] Layout/SpaceBeforeBlockBraces: Space missing to the left of {.
list.each_line.with_index{|line|
                         ^
cat.rb:8:26: C: [Correctable] Layout/SpaceInsideBlockBraces: Space between { and | missing.
list.each_line.with_index{|line|
                         ^^
cat.rb:8:26: C: [Correctable] Style/BlockDelimiters: Avoid using {...} for multi-line blocks.
list.each_line.with_index{|line|
                         ^
cat.rb:10:2: C: [Correctable] Layout/TrailingEmptyLines: Final newline missing.
}
 

1 file inspected, 7 offenses detected, 6 offenses auto-correctable

Rubocop发现了7个违法行为,其中6个可以自动纠正,在上面的输出中被标记为[Correctable]

管线

挑选基础设施

在挑选CI/CD基础设施供应商时,选择是很多的。从开源开发者的宠儿Travis CI,到宁愿自我托管其定制解决方案的企业团队的首选工具Jenkins,开发-运营工程师们有很多选择。

但根据我的经验,其中最简单的是GitHub工作流,这是一个GitHub原生的解决方案,允许你设置整个工作链,描述为YAML文件,可以根据特定的触发器来启动。我们可以在整个CI/CD管道中使用它们,从合并前对PR的检查到合并后的代码部署。有成百上千的预置动作(许多是官方维护的),可以让我们在建立端到端的管道时少走弯路。

当然,我们将使用GitHub工作流程作为我们的CI管道基础设施。我们的最终目标是让linting作为对我们的PR和提交的检查。只有通过检查的PR才是可合并的。

添加lint工作流

让我们看看在我们的案例中,工作流文件会是什么样子。创建一个新的目录.github/ ,在这个目录下创建一个名为workflows/ 的目录,并在这个目录下创建一个名为lint.yml 的文件:

# .github/workflows/lint.yml
name: Lint

on:
  push:
    branches:
    - master

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.0
        bundler-cache: true # runs `bundle install`
    - run: bundle exec rubocop .

lint工作流程由一个工作组成。该作业在每次推送到master 分支的事件中被触发,并执行三个步骤:

  1. actions/checkout:检查代码库的情况
  2. ruby/setup-ruby:
    1. 设置Ruby 3.0版本和Bundler的最新兼容版本
    2. 使用Bundler来安装Ruby 3.0中的所有包。Gemfile
  3. run:在整个当前工作目录中运行Rubocop。

一旦工作完成,我们就会得到我们的结果。在我们的例子中,它是一个大红叉。工作流执行失败,因为Rubocop发现了代码中的问题。日志显示了我们之前看到的同样的信息,并列出了Rubocop发现的罪行。

😢 我们最终会达到目的。

修复问题

我们希望我们的检查能够通过。没有人喜欢失败的检查。回到我们的本地设置,让我们使用Rubocop的自动修复功能来快速解决这些问题。首先,我们应该让Rubocop使用-a 标志来处理自动的东西:

$ bundle exec rubocop -a cat.rb
Inspecting 1 file
W

Offenses:

cat.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
# cat.rb
^
cat.rb:4:8: C: Security/Open: The use of Kernel#open is a serious security risk.
file = open(filename)
       ^^^^
cat.rb:8:15: C: [Corrected] Layout/ExtraSpacing: Unnecessary spacing detected.
list.each_line  do |line|
              ^
cat.rb:8:16: W: [Corrected] Lint/RedundantWithIndex: Remove redundant with_index.
list.each_line.with_index{|line|
               ^^^^^^^^^^
cat.rb:8:26: C: [Corrected] Layout/SpaceBeforeBlockBraces: Space missing to the left of {.
list.each_line.with_index{|line|
                         ^
cat.rb:8:26: C: [Corrected] Layout/SpaceInsideBlockBraces: Space between { and | missing.
list.each_line.with_index{|line|
                         ^^
cat.rb:8:26: C: [Corrected] Style/BlockDelimiters: Avoid using {...} for multi-line blocks.
list.each_line.with_index{|line|
                         ^
cat.rb:10:2: C: [Corrected] Layout/TrailingEmptyLines: Final newline missing.
}
 

1 file inspected, 8 offenses detected, 6 offenses corrected, 1 more offense can be corrected with `rubocop -A`

Rubocop解决了大部分报告的问题,包括在自动修复过程本身中引入的一个问题!7个问题中的6个已经被解决了,我们已经成功地减少了~70%的工作,而没有投入任何精力。

剩下的是一个不安全的自动修复和一个Rubocop无法自动修复的安全漏洞。我们可以解决这些问题:

  • 冻结的字符串字面注释丢失了。这是一个合理的添加到文件中的东西,所以我们让Rubocop使用更强大的自动修正标志-A
$ bundle exec rubocop -A cat.rb
Inspecting 1 file
C

Offenses:

cat.rb:1:1: C: [Corrected] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
# cat.rb
^
cat.rb:2:1: C: [Corrected] Layout/EmptyLineAfterMagicComment: Add an empty line after magic comments.
# cat.rb
^
cat.rb:6:8: C: Security/Open: The use of Kernel#open is a serious security risk.
file = open(filename)
       ^^^^

1 file inspected, 3 offenses detected, 2 offenses corrected
- file = open(filename)
+ file = File.open(filename)

提交并推送。我们现在是绿色的了!在这一点上,你应该为自己完成的工作拍拍屁股:

🥳 Yay!

保持清洁

现在你已经达到了代码质量的顶峰,我们需要确保它保持这种状态。这意味着我们需要确保没有一个PR对我们的代码库质量产生负面影响。为了对每个传入的PR进行检查,请在我们的lint工作流中添加pull_request 事件:

  on:
  on:
    push:
      branches:
      - master
+   pull_request:
+     branches:
+     - master

现在,为了测试我们的检查是否像预期的那样工作,我们需要用一些Rubocop会标记的代码制作一个PR。让我们重构脚本中与读取文件有关的行,以使用一个块:

  # cat.rb
  # frozen_string_literal: true

  filename = ARGV[0]

- file = File.open(filename)
- list = file.read
- file.close
+ File.open(filename) do |file|
+   list = file.read
+ end

list.each_line do |line|
  puts line
end

master ,查看一个新的分支。提交并推送到这个分支,并打开一个PR。你会看到检查失败了,因此,除非被管理员覆盖,否则该PR不能被合并。

🚧 我们的检查工作得很好!

🧠脑筋急转弯:你能确定为什么使用块的更新代码会被Rubocop标记吗?

🤷♂️提示:如果你想要一个提示,这里是PR的Rubocop输出。

$ bundle exec rubocop cat.rb
Inspecting 1 file
W

Offenses:

cat.rb:7:3: W: Lint/UselessAssignment: Useless assignment to variable - list.
  list = file.read
  ^^^^

1 file inspected, 1 offense detected

🧑💻答案:在区块内定义list ,意味着它在区块外不能被访问。这使得赋值毫无用处,并将导致后续使用该变量时出现错误。

💡教训:虽然是间接的,但代码分析有时也能帮助识别潜在的bug!

虽然我们投入了相当多的时间和精力,我们现在已经设置了检查和行动来监控我们的 repo 中的代码质量。但是,如果我们没有时间或者不想花费精力呢?我们毕竟是忙碌的开发者!我们可以考虑使用DeepSource。

可以考虑使用DeepSource。它通过各种静态代码分析器(包括linters和安全分析器)持续扫描每次提交的代码,以及每次拉动请求的代码,并可以自动修复其中的一些问题。DeepSource也有它为大多数语言定制的分析器,这些分析器被不断改进并保持最新。

它的设置令人难以置信的简单!你只需要在你的版本库根部添加一个.deepsource.toml 文件,DeepSource就会接收它。与在GitHub中设置几个工作流程相比,它花费的精力要少得多,而且最终的结果也更加精良。

version = 1

[[analyzers]]
name = "ruby"
enabled = true

[[transformers]]
name = "rubocop"
enabled = true

将繁琐的事情自动化

CI/CD管线是敏捷开发工作流程的精髓。增加功能、消除bug,并在生产中立即进行修改的能力可以带来非常大的变化。创业公司的生死取决于他们迭代的频率。

将静态分析整合到CI管道中,可以确保只有最干净、最合规的代码才能进入生产。静态分析只需要很少的时间来设置,消耗极少的资源,并且不会对测试/构建的时间产生明显的影响,静态分析可以为你的构建过程增加很多信心。

自信的迭代正在等待。直到下一次!