LabHub
学习 学习路径 课程

CI/CD 流水线

做一道拦住合并的门禁

在 LabHub 中继续学习

目标

使用 shell 编写测试运行器、覆盖率门禁、重试和隔离机制,并将这些信号合并成一个是否允许合并的判定。

为什么这很重要

门禁的价值不在于放行,而在于阻止。因此,运行器必须准确统计失败,覆盖率比较不能在边界值上出错,重试也必须设有上限。无限重试是在掩盖失败;隔离机制则是一种折中,它避免不稳定测试阻塞整个团队,同时必须在报告中留下记录。最终报告之所以重要,是因为读取该判定并阻止合并的不是人,而是分支保护中的 required status checks 等自动化机制。数值与判定不一致的报告会让门禁失去意义。

步骤

  1. 创建 /root/ci2/run-tests.sh <디렉터리> 并赋予执行权限。逐个运行参数所指定目录中的 *.sh 文件,最后用一行输出摘要 passed=<수> failed=<수> skipped=<수>。全部通过时退出码为 0。测试文件可能没有执行权限,因此要使用 bash "$f" 的形式运行。
  2. 即使其中有失败的测试,也要继续运行其余测试直至结束,最后以非 0 状态码退出。若有 2 个通过、1 个失败,摘要应为 passed=2 failed=1
  3. 收到空目录时,输出 passed=0 failed=0,退出码为 0。此时不要在目标目录内创建日志等文件。
  4. 创建 /root/ci2/gate-coverage.sh <커버리지파일> <임계값>。覆盖率文件只有一行,格式为 covered=80 total=100。百分比为 covered * 100 / total;达到或超过阈值时退出码为 0,低于阈值时返回非 0 状态码(相等时通过)。失败时输出实际百分比值。
  5. 创建 /root/ci2/retry.sh <최대시도횟수> <명령...>。命令成功后立即停止并返回 0;达到最大次数后仍全部失败,则返回非 0 状态码。实际尝试次数绝不能超过最大次数。
  6. 如果目标目录中存在 quarantine.txt,则其中列出的文件名(每行一个)不执行,而计入 skipped。被隔离的测试不计入 failed;其余测试全部通过时,退出码为 0。
  7. 创建 /root/ci2/gate.sh <테스트디렉터리> <커버리지파일> <임계값>。只有测试和覆盖率两个条件都通过时才返回退出码 0;任一条件失败都返回非 0 状态码。
  8. 创建 /root/ci2/gate-report.json。字段包括 tests.passedtests.failedtests.skippedcoverage.pctcoverage.thresholdcoverage.ok(布尔值 true/false)以及 decision(字符串 mergeblock)。如果 failed 大于等于 1,或者 pct 低于 threshold,则 decision 必须为 block;其他情况必须为 merge

参考

创建测试运行器

创建 /root/ci2/run-tests.sh <디렉터리> 并赋予执行权限。逐个运行参数所指定目录中的 *.sh 文件,最后用一行输出摘要 passed=<수> failed=<수> skipped=<수>。全部通过时退出码为 0。测试文件可能没有执行权限,因此要使用 bash "$f" 的形式运行。

调用方式为 /root/ci2/run-tests.sh <디렉터리>。逐个运行目录中的 *.sh,最后只输出一行摘要。评分创建的测试文件没有执行权限,因此必须使用 bash "$f" 运行。

即使失败也统计到最后

即使其中有失败的测试,也要继续运行其余测试直至结束,最后以非 0 状态码退出。若有 2 个通过、1 个失败,摘要应为 passed=2 failed=1

第一次失败时就停止,便无法看到全貌。请运行到最后,再以非 0 状态码退出。如果开启 set -e 后直接调用测试,运行器本身会终止。请使用 if bash "$f"; then ... else ... fi 直接处理退出码。

安全处理空输入

收到空目录时,输出 passed=0 failed=0,退出码为 0。此时不要在目标目录内创建日志等文件。

没有任何测试并不算失败。当 glob 未匹配任何内容时,请检查文件是否存在,避免把 *.sh 字符串本身作为文件名传入。不要在目标目录中创建日志文件。

覆盖率阈值门禁

创建 /root/ci2/gate-coverage.sh <커버리지파일> <임계값>。覆盖率文件只有一行,格式为 covered=80 total=100。百分比为 covered * 100 / total;达到或超过阈值时退出码为 0,低于阈值时返回非 0 状态码(相等时通过)。失败时输出实际百分比值。

格式为 gate-coverage.sh <파일> <임계값>,文件中只有一行 covered=80 total=100。边界值(相等时)应通过。使用整数除法会在边界处出错,请像 awk 'BEGIN{exit !(p+0 >= t+0)}' 这样进行浮点数比较。

有上限的重试

创建 /root/ci2/retry.sh <최대시도횟수> <명령...>。命令成功后立即停止并返回 0;达到最大次数后仍全部失败,则返回非 0 状态码。实际尝试次数绝不能超过最大次数。

格式为 retry.sh <최대횟수> <명령...>。成功后立即停止,绝不能超过最大次数。无限重试不是稳定化,而是在掩盖失败,因此全部尝试失败后必须返回非 0 状态码。

隔离不稳定测试

如果目标目录中存在 quarantine.txt,则其中列出的文件名(每行一个)不执行,而计入 skipped。被隔离的测试不计入 failed;其余测试全部通过时,退出码为 0。

目标目录的 quarantine.txt 中每行写有一个文件名。不要执行这些文件,而应计入 skipped。隔离测试不计入 failed,因此其余测试通过时退出码为 0。

复合门禁

创建 /root/ci2/gate.sh <테스트디렉터리> <커버리지파일> <임계값>。只有测试和覆盖率两个条件都通过时才返回退出码 0;任一条件失败都返回非 0 状态码。

gate.sh <테스트디렉터리> <커버리지파일> <임계값> 只需调用前面创建的两个脚本并合并判定。任一失败即为失败。将各次调用的退出码保存到变量中,还可以输出是哪一项阻止了通过。

判定与数值一致的报告

创建 /root/ci2/gate-report.json。字段包括 tests.passedtests.failedtests.skippedcoverage.pctcoverage.thresholdcoverage.ok(布尔值 true/false)以及 decision(字符串 mergeblock)。如果 failed 大于等于 1,或者 pct 低于 threshold,则 decision 必须为 block;其他情况必须为 merge

/root/ci2/gate-report.json 中的 decision 必须从数值推导。如果 failed 大于等于 1,或 pct 低于 threshold,则为 block,否则为 merge。coverage.ok 应为 JSON 布尔值(true/false),而不是字符串。