RTL 回归 Jenkinsfile 模板:夜间定时、结果归档与失败通知
更新时间:2026-11-03。模板实战页,属于芯片设计入门系列。上一篇 仿真回归控制Shell脚本模板 解决了「一条命令跑完所有种子、退出码给 CI 用」,本篇把这条命令挂到 Jenkins 上:夜里自动跑、结果能留档、失败有人被叫起来。模板包含一份声明式 Jenkinsfile 和一个最小 bash 回归 runner,全部用两个 mock 用例(一个 PASS、一个 FAIL)在本机干跑验证过,不依赖任何 EDA 工具。回归频率怎么定见 回归该跑多勤,归档丢失的真实排错见 Jenkins 回归产物缺失。
本文要回答的问题
- 一份夜间 RTL 回归 Jenkinsfile 最少要包含哪三段?
- cron 触发器为什么写
H 21而不是0 21? - 两个仿真器并行跑,stage 与 agent 标签怎么组织?
- 日志和波形为什么只在失败时归档,JUnit XML 却每次都要发布?
- 失败通知发邮件还是发企业 IM webhook,凭据怎么放才不泄密?
- runner 的退出码和 JUnit XML 怎么设计,Jenkins 才能直接接住?
一、三段式结构总览
先给整体心智模型。这张卡把整条夜间流水线压成三段:触发与执行、结果留档、失败外传。三段缺一段都会在真实项目里被教做人——没有定时触发就得人肉记着点;没有归档,失败现场在 cleanWs 后灰飞烟灭;没有通知,红灯要等第二天有人自己去看面板。
读这张卡注意三段的不对称:第一段最厚(四件事),是模板的主体;第二段两条规则看似矛盾(JUnit 总归档、日志失败才归档),原因第四节展开;第三段只做「把红灯送到人面前」一件事,通知内容必须带上 seed,否则收到告警也无法复现。
下面的代码照这个顺序分。先看 runner——它是 pipeline 调用的内核,再看 Jenkinsfile 怎么把它挂起来。
二、最小回归 runner:逐用例、独立日志、JUnit XML
Jenkinsfile 里每个并行轴最终都调一个 shell runner。runner 的契约很简单:发现 tests/<name>/run.sh 逐个执行,每个用例独立日志,结束打印一行机读 summary 并产出 JUnit XML,有失败则退出码为 1。这与 仿真回归控制Shell脚本模板 是同一套纪律:用 set -u 不用 set -e,失败也要跑完再汇报;区别是本篇 runner 额外吐 XML,让 Jenkins 的 junit 步骤直接生成趋势图。
#!/usr/bin/env bash
# run-regression.sh -- RTL 回归最小 runner 模板
set -u
TEST_DIR="${TEST_DIR:-tests}"
LOG_DIR="${LOG_DIR:-logs}"
OUT_XML="${OUT_XML:-results/regression.xml}"
TESTFILTER="${TESTFILTER:-}" # 只跑名字包含该串的用例
SEED="${SEED:-1}" # 透传给每个用例,失败现场的唯一凭证
JOBS_TIMEOUT="${JOBS_TIMEOUT:-300}"
mkdir -p "$LOG_DIR" "$(dirname "$OUT_XML")"
rm -f "$LOG_DIR"/*.log
xml_escape() {
sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g' -e 's/"/\"/g'
}
total=0; npass=0; nfail=0; failed_names=""
: > "$OUT_XML.tmp"
compgen -G "$TEST_DIR/*/run.sh" > /dev/null || { echo "[REGRESS] no testcase"; exit 2; }
for tc in "$TEST_DIR"/*/run.sh; do
name="$(basename "$(dirname "$tc")")"
[ -z "$TESTFILTER" ] || case "$name" in *"$TESTFILTER"*) ;; *) continue;; esac
total=$((total + 1)); log="$LOG_DIR/${name}.log"
t0=$(date +%s)
if command -v timeout >/dev/null 2>&1; then
SEED="$SEED" timeout "$JOBS_TIMEOUT" bash "$tc" > "$log" 2>&1
else
SEED="$SEED" bash "$tc" > "$log" 2>&1
fi
rc=$?; dt=$(( $(date +%s) - t0 ))
if [ "$rc" -eq 0 ]; then
echo "[REGRESS] $name PASS (${dt}s)"; npass=$((npass + 1))
printf ' <testcase classname="rtl" name="%s" time="%s"/>\n' "$name" "$dt" >> "$OUT_XML.tmp"
else
echo "[REGRESS] $name FAIL rc=$rc (${dt}s, log=$log)"
nfail=$((nfail + 1)); failed_names="$failed_names $name"
detail="$(tail -n 20 "$log" | xml_escape)"
{
printf ' <testcase classname="rtl" name="%s" time="%s">\n' "$name" "$dt"
printf ' <failure message="rc=%s">%s</failure>\n' "$rc" "$detail"
printf ' </testcase>\n'
} >> "$OUT_XML.tmp"
fi
done
{
printf '<?xml version="1.0" encoding="UTF-8"?>\n'
printf '<testsuite name="rtl-regression" tests="%s" failures="%s" seed="%s">\n' \
"$total" "$nfail" "$SEED"
cat "$OUT_XML.tmp"; printf '</testsuite>\n'
} > "$OUT_XML"
rm -f "$OUT_XML.tmp"
echo "[REGRESS] SUMMARY total=$total pass=$npass fail=$nfail seed=$SEED"
if [ "$nfail" -gt 0 ]; then echo "[REGRESS] FAILED:$failed_names"; exit 1; fi
echo "[REGRESS] ALL PASS"; exit 0几个设计点直接决定上层 pipeline 的写法。LOG_DIR 与 OUT_XML 都做成环境变量,两个并行轴才可以分别传 logs/iverilog、logs/verilator,在同一 workspace 里互不覆盖。失败用例的日志尾部二十行直接内嵌进 <failure> 标签,Jenkins 测试结果页不用下载归档就能看到失配明细;但内容要过 xml_escape,仿真器日志里的尖括号不转义会让 XML 解析整个炸掉。timeout 用 command -v 守卫,是因为 macOS 默认没有该命令——模板在开发机和 CI 节点上都要能跑。真实工程里把 mock 的 tests/<name>/run.sh 换成「编译 DUT 加 testbench、跑仿真、grep 判定」的封装即可,编译与仿真的组织方式可照抄 仿真 Makefile 模板 或参考 Makefile 与 Shell 的 EDA 管理分工。
三、Jenkinsfile 完整模板
下面这份声明式 pipeline 可以直接放进仓库根目录。顶层 agent none 配合并行轴各自的 agent { label },让两个仿真器跑在不同节点上;只有一台机器时把两个 label 改成同一个即可,结构不用动。
pipeline {
agent none
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '30'))
}
parameters {
string(name: 'TESTFILTER', defaultValue: '', description: '只跑名字包含该串的用例,留空跑全部')
string(name: 'SEED', defaultValue: '1', description: '随机种子,复现失败时填上一轮日志里的 seed')
choice(name: 'SIMULATOR', choices: ['all', 'iverilog', 'verilator'], description: '选择仿真器轴')
}
triggers { cron('H 21 * * 1-5') } // 工作日夜间,H 让 Jenkins 在该小时内自动打散
environment {
NOTIFY_MAIL = credentials('rtl-regression-notify-mail')
}
stages {
stage('Parallel Regression') {
parallel {
stage('iverilog') {
when { expression { env.SIMULATOR == 'all' || env.SIMULATOR == 'iverilog' } }
agent { label 'sim-iverilog' }
steps {
sh 'make compile SIM=iverilog'
sh "TESTFILTER='${params.TESTFILTER}' SEED='${params.SEED}' " +
"LOG_DIR=logs/iverilog OUT_XML=results/iverilog.xml ./run-regression.sh"
}
}
stage('verilator') {
when { expression { env.SIMULATOR == 'all' || env.SIMULATOR == 'verilator' } }
agent { label 'sim-verilator' }
steps {
sh 'make compile SIM=verilator'
sh "TESTFILTER='${params.TESTFILTER}' SEED='${params.SEED}' " +
"LOG_DIR=logs/verilator OUT_XML=results/verilator.xml ./run-regression.sh"
}
}
}
post {
always { junit testResults: 'results/*.xml', allowEmptyResults: true }
failure {
archiveArtifacts artifacts: 'logs/**/*.log, results/*.xml, failures/**/*.vcd',
allowEmptyArchive: true, onlyIfSuccessful: false
}
}
}
}
post {
failure {
emailext subject: "RTL 回归失败:${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """RTL nightly regression FAILED.
Seed: ${params.SEED} Filter: ${params.TESTFILTER}
Console: ${env.BUILD_URL}console
失败用例见归档 logs/ 与 JUnit 报告。""",
recipientProviders: [requestor(), culprits(), upstreamDevelopers()],
to: env.NOTIFY_MAIL, attachLog: false
// 方案 B(可选):企业 IM webhook,与邮件二选一,URL 放凭据不入库
// withCredentials([string(credentialsId: 'notify-webhook', variable: 'NOTIFY_WEBHOOK')]) {
// sh '''curl -fsS -X POST "$NOTIFY_WEBHOOK" -H 'Content-Type: application/json' \
// -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"RTL 回归失败 ${JOB_NAME} #${BUILD_NUMBER} seed=${SEED} ${BUILD_URL}\"}}"'''
// }
}
always { cleanWs(deleteDirs: true, disableDeferredWipeout: true) }
}
}模板各段的语义可以对着下面这张阶段图读:主轴是两轴并行编译加回归,汇合点在结果发布;任何一轴失败,归档与通知两个旁路才触发,而清理无条件收尾。
图里蓝色是入口、橙色是干活的两个轴、紫色是发布、红色两个旁路只在失败时走、绿色是无条件收尾。注意归档与通知都排在清理之前——Jenkins 的 post 块按条件分块执行,同一轮里 failure 先于 always,但这只是惯例不是契约,把归档只写进 failure、清理写进最外层 always,语义上就彻底不会误删现场。
cron('H 21 * * 1-5') 是 Jenkins 特有的散列语法:H 会按 job 名算出一个该小时内的固定分钟,让一批夜间任务在 21:00 到 21:59 之间错开,避免节点和 license 服务器整点齐发被打爆。disableConcurrentBuilds 防止上一轮还没跑完新一轮又启动;参数化构建保留了白天手动复现的入口——夜里收到告警,直接用上一轮的 SEED「Build with Parameters」即可。两个仿真器轴的差异与取舍见 Verilator 与 VCS 的主要差异,大规模跨机调度再参考 回归管理方法论。
四、参数、凭据与通知取舍
模板里所有可配置项集中在这张表,落地时按团队环境改值,不要把真实值散进 Groovy 字符串。
| 参数/变量 | 来源 | 默认 | 作用与注意 |
|---|---|---|---|
TESTFILTER | 构建参数 | 空 | 子串匹配用例名,空为全量;修 bug 后只跑相关用例 |
SEED | 构建参数 | 1 | 透传 runner 再透传用例;随机回归失败后复现的唯一凭证 |
SIMULATOR | choice 参数 | all | 控制只跑单轴,常用于某仿真器 license 紧张时降载 |
LOG_DIR / OUT_XML | runner 环境变量 | logs / results/regression.xml | 两轴分别指向不同子路径,防并行互踩 |
NOTIFY_MAIL | Jenkins credentials | 无 | 收件人列表放凭据中心,轮换不改正文 |
NOTIFY_WEBHOOK | 可选凭据 | 无 | IM 机器人地址,withCredentials 注入,注释方案取消注释即用 |
BUILD_URL / JOB_NAME | Jenkins 内置 | 自动 | 通知正文里的链接只用内置变量,不手写站点地址 |
邮件与 webhook 的取舍:邮件适合需要抄送一长串干系人、附带 culprits 自动定位「谁的提交引入失败」的团队,emailext 的 recipientProviders 白送这个能力;IM webhook 的优势是到达率高、适合小团队「红灯即喊话」,但消息要短,长日志仍然靠归档和 BUILD_URL。两者只开一个,夜里同一轮失败邮件加群消息各发一遍是纯粹的噪音。文件里绝不能出现真实 webhook 或 token——curl 命令里一律 "$NOTIFY_WEBHOOK" 占位,凭据在 Jenkins 凭据中心注入。
五、干跑验证
本机没有 Groovy/Jenkins 工具链,无法真正解析 Jenkinsfile,所以证据分两部分:runner 与两个 mock 用例真实执行;Jenkinsfile 如实记录 groovy 不可用,改按声明式 pipeline schema 逐项人工核对(顶层块、cron 字段、parallel 轴的 agent/when、post 各条件块顺序、凭据占位)。两个 mock 用例一个打印 TEST PASS 退出 0,一个模拟向量失配退出 1。
实录里抓三个数字:全量轮 SUMMARY total=2 pass=1 fail=1 seed=1、FAILED: tc_fail、runner 退出码 1,Jenkins 的 sh 步骤拿到非零码就会把该轴标红;产出的 regression.xml 里 tests="2" failures="1",失败用例带内嵌 <failure> 明细,junit 步骤可直接消费。过滤轮 TESTFILTER=tc_pass SEED=314 只跑一个用例,pass=1 fail=0、退出码 0,对应白天修完缺陷后的定向复跑路径。
代码获取:Jenkinsfile、runner、两个 mock 用例与证据采集脚本都在公开仓 experiments/tpl-jenkinsfile-rtl-regression,克隆后执行 ./run-regression.sh 即可复现 1 PASS 1 FAIL、退出码 1 的全量轮,bash run-evidence.sh 一键跑完语法校验、结构核对清单与两轮回归。
六、常见失败排查
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 两轴互相覆盖日志 | 没给各轴单独传 LOG_DIR/OUT_XML | 按模板传 logs/iverilog、logs/verilator |
| 归档里没有失败波形 | 波形未按失败 dump,或路径不匹配 artifact pattern | 用例只在失败时 dump 到 failures/<name>.vcd,通配符已含该目录 |
| JUnit 页报 XML 解析错 | 日志原文内嵌时未转义 <、& | 保留 runner 里的 xml_escape,别图省事删掉 |
| 通知没发出但构建标红 | 凭据 ID 不存在或 emailext 插件未装 | 先在凭据中心建 rtl-regression-notify-mail,插件装 Email Extension |
| 夜间任务整点集体排队 | cron 写成了 0 21 | 换成 H 21 让 Jenkins 散列打散 |
| 收了告警无法复现 | 通知或日志没带 seed | 标题与 summary 都带 seed,用同 SEED 手动带参构建 |
| 归档构建完就消失 | cleanWs 先于归档执行 | 归档放内层 post.failure,清理放最外层 post.always |
最后一行是 Jenkins 回归最常见的真实事故,完整排查路径见 Jenkins 回归产物缺失。
实验环境
- 机器/系统:Mac mini(Apple Silicon)/ macOS 26.5.2(Darwin 25)
- 工具版本:GNU bash 3.2.57(
bash --version真实输出);本机无 groovy,Jenkinsfile 按声明式 schema 人工核对,核对清单与结论已记录在原始日志 - 日期:2026-11-03
- 完整命令:
bash run-evidence.sh(含bash -n run-regression.sh、groovy 可用性探测与人工核对、全量双用例回归、JUnit XML 输出、TESTFILTER=tc_pass SEED=314过滤轮)
这一篇在知识体系里的位置
本篇是「仿真回归管起来」系列的 CI 挂载层:内核脚本承接 仿真回归控制Shell脚本模板,单机构建件的写法对照 仿真 Makefile 模板,调度频率依据 回归该跑多勤,规模化后走向 回归管理方法论 与 LSF 网格回归;本篇用到的归档事故排错在 Jenkins 回归产物缺失。
总结
RTL 回归 Jenkinsfile 模板的核心是三段:cron 加参数化负责夜里自动跑和现场复现,JUnit 每次发布、日志波形只在失败时归档负责留证据,email-ext 或 webhook 负责把红灯连同 seed 送到人面前。两轴并行靠各自 agent 标签与独立 LOG_DIR 隔离,凭据一律走 credentials,清理无条件收尾但永远排在归档之后。配套 bash runner 产出机读 summary 与 JUnit XML,经双 mock 用例干跑验证:全量 total=2、pass=1、fail=1、退出码 1,过滤轮退出码 0。