工作流模板


本文介绍如何使用 GitHub Actions(GHA)为 Exercism 语言轨道搭建持续集成(CI)工作流。它提供最佳实践和示例,供你参考,帮助你打造自己快速、可靠、健壮的 CI 工作流。这个文件夹里的 GHA 工作流可以改造成适配任何 CI,因为基本结构保持不变。

本文将:

  • 勾勒理想的 CI 工作流
  • 讨论注意事项和建议
  • 为你提供一些可用的模板
  • 留给你一份从 Travis 迁移的指南

这些工作流文件的示例实现可以在exercism/javascript中找到。

求助:这看起来工作量_很大_ 😓

文档接下来的部分旨在解释这些工作流如何运作。如果你很赶时间,只想从 Travis 或 Circle 切换到 GHA,而不优化 PR 脚本,可以看看我们关于从 Travis 迁移的约 10 分钟指南。

轨道的 CI 操作

用来检查代码仓库内容完整性的推荐操作如下:

  1. 运行configlet lint,以检查config.json
  2. 检查 stub
  3. 检查文档(v3要求新增文件;以后这可能会移到configlet)
  4. 使用 "maintainers" 配置对练习运行 lint
  5. 使用示例/范例文件测试练习(可以包含构建步骤)

还可能有针对特定轨道的操作。例如:

  1. 检查练习配置的完整性
  2. 检查练习文件的格式

也许你还想要更多便利性检查,例如:

  1. 确保 CONTRIBUTING 存在
  2. 确保依赖有合理的锁定文件
  3. 确保 Markdown 文件里的链接有效
  4. ……

建议

检查的运行频率

对每个操作,想清楚它应该多久运行一次。

  • configlet lint 非常重要(因为config.json一旦出问题,轨道就可能崩溃),所以它大概应该一直运行,不过每次提交只需运行一次。
  • 文件是否存在及其完整性,每次提交只需检查一次。
  • 如果某个轨道要在多个运行时版本或编译器版本下运行,那么构建/测试练习就应该针对每个受支持的版本都跑一遍。
  • PR _大概_只需要对_新增_或_改动_的文件运行操作,但既然一个文件可能影响整个练习,那么当练习的某个文件发生变化时,更稳妥的做法是针对整个_练习_运行这些操作。

把应该运行的操作同时做成可以在本地运行,会非常有帮助。这意味着真正干活的脚本也能手动运行。为此,不要把操作内联到工作流文件里,而要创建一个独立脚本。例如,检查 stub 完全可以直接在工作流文件里用 bash 搞定,但这里建议改为新建一个可执行脚本scripts/ci-check。

“可是这个命令很短啊,比如eslint . --ext ts --ext tsx。”

当这个命令需要更新时,你现在得在文档、工作流文件以及_维护者的脑子里_的所有地方都更新一遍。把它抽成一个脚本就能解决这一切。而且读工作流文件本身就非常让人头大。

练习发生变动时的 PR 检查

scripts/pr和scripts/pr-check脚本(见模板)运行时接收多个参数,每个参数对应这个 PR 里改动或新增的一个文件。例如,如果two-fer有更新,调用可能长这样:

scripts/pr exercises/two-fer/README.md exercises/two-fer/.meta/example.ext

建议针对发生变动的_练习_运行操作,而不是针对发生变动的_文件_。因为改动一个文件很可能会牵动整个练习(想想配置、软件包)。

还没准备好? / 太复杂?

在实现这项优化之前,完全可以放心地先忽略它!迁移指南暗示可以稍后再加。如果忽略传入的参数,所有检查都会在所有练习上运行。这完全没问题。只是会花更长时间。

完整性检查

如果轨道有一个单独的“顶层”依赖文件和/或其他配置文件,就加一个完整性步骤(它和scripts/sync或bin/sync一起存在,后者会把所有配置文件复制到所有练习),用来确保顶层/基础文件与复制到各练习目录里的文件一致。这样一来,依赖可以更新、在整个代码仓库里同步,我们也能确保所有练习的配置一致。

实现这一点的常见方法是使用校验和。Ubuntu(以及各种其他 Linux 发行版)自带一个叫sha1sum的工具,但用_任何_方法把配置文件哈希或归约成一个校验和值(md5、sha1、crc32)都行:

$ sha1sum README.md
cd58091c5043bf21f00d39ff1740d8b2976deeff *README.md

安全检查

如果轨道使用了需要访问 GitHub 令牌或其他密钥的其他工作流,最佳实践是把工作流里用到的所有操作固定到某个特定提交。详细信息见 GitHub 的安全加固指南。

例如:

- uses: julia-actions/setup-julia@v1
+ uses: julia-actions/setup-julia@d26d1111976eae5f00db04f0515ab744ec9cd79e # 1.3.1

如果工具链用锁定文件来管理依赖,可以考虑把它提交进代码仓库,并在工作流文件里使用“冻结的锁定文件”。例如:npm ci、yarn install --frozen-lockfile和bundle install --frozen。这能确保更改依赖时锁定文件是最新的,并防止恶意软件包混进来。

模板

在这个目录里,至少有下面这些模板:

  • configlet.yml:这个工作流会拉取最新的 configlet 二进制文件并对代码仓库运行 lint。每次提交都会运行。对于 PR,会在实际提交和“合并后”的代码树上运行。
  • ci.yml:这个工作流只在 main 分支上运行,每次提交运行一次。
    1. 对所有练习运行 'pre-check' 命令(检查 stub、lint、文档等)
    2. 对所有练习的多个版本运行 'ci' 命令(构建并测试)
  • pr.ci.yml:这个工作流只在 PR 上运行,每次提交运行一次。
    1. 对改动的文件运行 'pre-check' 命令(检查 stub、lint、文档等)
    2. 对改动的练习的多个版本运行 'ci' 命令(构建并测试)

非 PR 工作流也可以通过workflow_dispatch触发。

每个文件顶部都列出了需要提供哪些“脚本”。如果你想把这些做成二进制文件,就把scripts/xxx换成bin/xxx。有些工具链会_要求_二进制文件放在bin文件夹里。

  • scripts/ci:一个脚本,应该用示例解答对所有练习进行构建和测试,并与测试比对
  • scripts/ci-check:一个脚本,应该对所有练习运行 lint,并可选地检查 stub、配置完整性等
  • scripts/pr:和scripts/ci相同,但只运行根据输入路径解析出的练习
  • scripts/pr-check:和scripts/ci-check相同,但只对根据输入路径解析出的文件或练习运行

疑难解答

如果你遇到任何问题,或者想让人帮你审阅工作流,请 @ 一下@exercism/github-actions团队。

改动了某个本应触发所有练习 CI 运行的顶层文件

在撰写本文时,pr.ci.yml只支持基于“扩展名”的测试。理想情况下,它应该更新为在某些文件发生变化时始终触发(比如用于运行测试的二进制文件)。不过,这类改动往往很少发生,而且由维护者来做,所以ci.yml在 main 上始终对所有东西运行,大概已经足够安全了。

在 Windows 上创建了 scripts/xxx 文件,结果它在 {other OS} 上不工作

默认情况下,在 Windows 上创建的文件,其 git 索引里不会嵌入关于可执行权限的元数据,因为 Windows 的权限模型不一样。Git 默认会使用 git 索引的元数据来判断文件在基于 POSIX 的系统上是否应该可执行,因此会让scripts/xxx文件变成不可执行。

git update-index --chmod=+x scripts/xxx
git commit -m "Make scripts/xxx executable"