用途
该技能面向发布流水线中“靠时序运气才通过”的 CI 步骤。这类步骤并非新引入的竞态,而是从一开始就写错,只是被机器当时的速度掩盖;运行器镜像更新改变时序后它们才开始失败,因此“弄坏”它们的提交通常与问题无关。
最昂贵的形态不是“间歇性失败”,而是“成功却产出错误工件”:硬失败只需重跑一次,而报告成功却把零字节文件、错误页或空挂载点交给下游任务,会耗费大量排查时间,且回溯信息指向错误来源。
主要能力与陷阱
挂载点
不要硬编码挂载点,应捕获工具实际报告的挂载点。名称冲突会改变工具报告的答案,而不会有其他警告。示例(改编自包装脚本,名称已泛化):
MOUNT_POINT="$(hdiutil attach -nobrowse -noautoopen "$RW_DMG" 2>/dev/null \
| grep -Eo '/Volumes/.*$' | tail -1 || true)"
[[ -d "$MOUNT_POINT" ]] || { echo "Could not mount $RW_DMG" >&2; exit 1; }
VOL_NAME="$(basename "$MOUNT_POINT")"
下游全部从 $MOUNT_POINT 和 $VOL_NAME 派生。若脚本在静默重挂载到 /Volumes/<Name> 1 后仍写入 /Volumes/<Name>,就会写进上一轮的卷,产出缺少其自认为已添加内容的输出。
清理时机
应在开始前清理,而不仅在结束后清理。上一轮的清理可能尚未完成,而只在结尾运行的清理在主体失败时根本不会执行:
detach_stale_volumes() {
for v in /Volumes/<Name> /Volumes/<Name>\ *; do
[[ -e "$v" ]] && hdiutil detach -force "$v" >/dev/null 2>&1 || true
done
}
detach_stale_volumes
for attempt in 1 2 3 4 5; do
MOUNT_POINT="$(hdiutil attach … | grep -Eo '/Volumes/.*$' | tail -1 || true)"
[[ -d "$MOUNT_POINT" ]] && break
detach_stale_volumes; sleep 3
done
注意通配符也覆盖带后缀的名称——那正是此前冲突留下的残留。
卸载也应强制并重试。下一轮从本轮遗留状态开始,因此主体末尾尽力而为的卸载值得配一个短重试循环。
下载
优先使用在 HTTP 错误时显式失败的下载器。标准库流 API 不会跟随切换协议的重定向——它返回重定向响应本身,复制该流会把重定向自带的 HTML 页面以工件文件名保存到磁盘。下载看起来成功了,只剩校验和能发现它。带显式标志的外部下载器则会失败:
ProcessBuilder(
"curl", "-fsSL", // --fail: non-zero on HTTP errors instead of saving the error body
// -L: follows redirects across protocols and mirrors
"--retry", "5", "--retry-delay", "5",
"--retry-all-errors", // plain --retry does NOT cover a mid-transfer receive failure;
// it retries only HTTP 5xx/408/429 and connection errors
"-o", target.absolutePath, url,
).inheritIO().start().waitFor()
即使退出码为零也要检查结果,并删除部分文件。否则空或截断的下载会被缓存,每次重试都复现它:
check(exit == 0 && target.exists() && target.length() > 0) {
if (target.exists()) target.delete()
"download failed (exit $exit) for $url"
}
镜像源如同抽奖,因此应校验内容而非信任来源。发布 URL 每次请求可能重定向到不同镜像。将下载与源码中固定的摘要值配对校验,这是镜像返回其他内容时唯一仍成立的断言。
缓存工件就是缓存失败。下载前的任何缓存层都会永久保留坏文件。让失败路径执行删除,并让缓存键包含标识该发布的内容,而不仅是版本号。
验证方式
时序运气型步骤有可 grep 的特征。在 shell 步骤和构建脚本中查找:
grep -rn 'Volumes/\|/mnt/\|/tmp/[a-z-]*/' --include='*.sh' --include='*.yml' scripts .github
grep -rn 'openStream\|URL(\|readBytes()' --include='*.kts' --include='*.gradle' .
grep -rn 'sleep ' --include='*.sh' --include='*.yml' scripts .github
对应三类信号:工具本应自行报告路径却使用固定路径;可能发生 HTTP 错误却使用标准库抓取;用裸 sleep 代替条件判断——那是一个缺少注释的时序假设。
然后对每一步追问:如果这一步做错了,任务还会是绿色的吗?每个“会”都是添加工件断言的位置——存在性、大小、类型、摘要——应加在工件产生处,而不是让后续任务去发现。
适用场景与限制
适用于排查和加固 CI/发布流水线中依赖时序、挂载、下载与缓存的步骤。资料未提供该技能的安装命令、平台支持范围、收费方式或具体使用步骤。