重跑率 31% → 6%,变更失败率 12% → 4%:CI/CD 流水线治理实战的 5 个配置

我们流水线压到 7 分钟之后,我统计了最近 300 次构建:31% 的绿色是重跑出来的。红灯点了重跑,绿了就合并,检查等于没做。

两个月前开始治理,没加任何新工具,改了 5 个配置。现在重跑率 6%,变更失败率从 12% 掉到 4%。下面按顺序贴出来,都能直接抄。

第一步:先把重跑率变成数字

治理之前得先能看见。用 gh api 拉最近 100 次构建,数 run_attempt 大于 1 的有多少:

bash 复制代码
#!/usr/bin/env bash
# rerun_rate.sh
REPO="your-org/your-repo"
gh api "repos/${REPO}/actions/runs?per_page=100" \
  --jq '{total: (.workflow_runs | length),
         rerun: ([.workflow_runs[] | select(.run_attempt > 1)] | length)}'

输出类似 {"total":100,"rerun":31}。我把它挂成每周一早上跑的定时任务,结果直接进周复盘。

有个细节:per_page=100 拉的是最近 100 次,不管时间跨度。发版频率低的仓库,改成按 created 时间过滤更准。

第二步:flaky 测试隔离,不是消灭

先说结论:别指望一次消灭 flaky,先隔离,再逐个修。

重试从全局改成显式标记。装 pytest-rerunfailures,不配全局 --reruns,只在确认过失败原因是环境类的测试上打标记:

python 复制代码
import pytest

@pytest.mark.flaky(reruns=2, reruns_delay=3)
def test_query_order_status():
    ...

隔离集用 marker 管理,先在 pyproject.toml 里注册:

toml 复制代码
[tool.pytest.ini_options]
markers = ["quarantine: flaky 候补,PR 流水线不跑"]

CI 侧分两条线。PR 流水线跑排除隔离集的用例;另建一个 nightly 工作流,凌晨跑全量,不带任何重试:

yaml 复制代码
# .github/workflows/nightly.yml
on:
  schedule:
    - cron: "0 18 * * *"   # UTC,北京时间凌晨 2 点,避开白天构建高峰
jobs:
  strict-full:
    runs-on: ubuntu-latest
    steps:
      - run: pytest

nightly 挂了谁,谁就是 flaky 候补(也可能是真回归,人工判断一下),第二天修。修一个,从 quarantine 放出来一个。两个月我们隔离了 14 个,修掉 11 个,PR 流水线基本不再重跑。

第三步:门禁分级,阻断只留机器能判定的

之前 23 个门禁全卡合并,后来砍到 9 个。原则一句话:能百分之百机器判定的才阻断,其余全改成报告。

yaml 复制代码
jobs:
  # 阻断级:必须绿才能合并
  block-unit:
    steps:
      - run: pytest tests/unit
      - run: gitleaks detect --no-banner --redact

  # 报告级:挂了不挡合并,评论到 PR 让人自己看
  report-sonar:
    steps:
      - run: sonar-scanner
      - if: failure()
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh pr comment ${{ github.event.number }} \
            --body "SonarQube 有新增问题,详见 Action 日志,合不合并你自己定。"

配套改分支保护规则:Required checks 里只勾 block- 开头的 job。report 级的 job 挂了在 PR 上照样显示红叉,但不拦人。

SonarQube 严重问题、覆盖率阈值、许可证检查,我们全挪到了报告级。挪过去之后没有任何一个变成没人看------红叉还在,只是不再拿十几个检查消耗人的判断力。

第四步:依赖缓存留痕,出事两分钟定位

缓存翻过车的人才知道这个的价值。我们之前 pip 缓存里躺了个过旧的 patch 版本,本地和 CI 行为对不上,查了两天。

现在缓存照用,但每次构建把依赖清单存档:

yaml 复制代码
- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${{ runner.os }}-${{ hashFiles('requirements.lock') }}

- run: pip freeze > deps-${{ github.sha }}.txt
- uses: actions/upload-artifact@v4
  with:
    name: dependency-snapshot
    path: deps-*.txt
    retention-days: 90

key 里带 hashFiles,锁文件变了缓存自动失效。真出"本地好好的 CI 坏了"这种事,下载 90 天内对应那次构建的 artifact 和本地 diff,两分钟出结论。

不拦,只留证据。比每次重建缓存省的那一分钟值。

第五步:阶段开关化,给"敢删"一个试用期

流水线里有不少"当年觉得有用"的阶段。现在每个非核心阶段都挂开关:

yaml 复制代码
e2e-smoke:
  if: vars.PIPELINE_E2E_SMOKE != 'off'
  steps: ...

vars 是仓库级变量,Settings → Variables 里改,不动代码、不用提 PR。

玩法:每个季度挑一个最可疑的阶段,开关拨到 off,观察两周。没出事,下个 PR 直接把 job 删掉。到现在我们删了 5 个阶段,没有一个加回来。

开关不是长期留着用的,是给删除一个安全的试用期。

前后数据

指标 治理前 治理后
重跑率(最近 100 次构建) 31% 6%
变更失败率 12% 4%
平均恢复时间 3 小时 40 分钟
流水线时长 7 分钟 7 分钟(没变快)

注意最后一行,流水线没变快。这套东西治的不是速度,是绿色里掺了多少次"再来一次"。

注意事项

  1. 重跑率统计要排除手动运维场景,我们按 run_attempt 统计前会过滤掉 event 为 workflow_dispatch 的构建,不然数字虚高。
  2. flaky 标记别滥用,只打在确认过是环境依赖类失败上的(超时、端口占用)。断言错了还打重试,等于把 bug 藏起来。
  3. 报告级门禁的 PR 评论要控制条数,同一个 PR 用固定评论更新,别一次失败刷一条。

先从第一步开始,今天就统计一下你们组的重跑率。统计完这个数,你大概率就知道该先动哪一步了。

你们流水线的重跑率是多少?评论区报个数。后面我会写那 11 个 flaky 具体怎么修的案例,想看的点个关注。

声明:本文选题与实操经验出自我本人,AI 用于资料整理与文字校对。

相关推荐
考虑考虑13 小时前
docker compose环境变量替换
运维·后端·自动化运维
本尊粥1 天前
网站一改版自动化脚本就废?聊聊自愈回放的完整设计
自动化运维
Ramble_Naylor1 天前
Forgejo
ci/cd·团队开发·产品经理·源代码管理
あ-1 天前
企业 DevOps + 协同办公 + 可观测性 + 网络基础设施平台
运维·网络·devops
smartpi_ai1 天前
空调内机做语音控制选哪颗?CI-03T 的红外码库优势、“5V 供电≠3.3V 串口“的电平误区与声学结构规范
人工智能·ci/cd·语音识别
SkyWalking中文站2 天前
Horizon UI 1.0 正式发布:SkyWalking 新一代控制台接棒 Booster
运维·监控·自动化运维
帷幕落秋2 天前
Nginx 系列实战(三):LNMP 完整搭建,动静分离的学习总结
运维·自动化运维
帷幕落秋2 天前
一文吃透 Nginx 虚拟主机:原理 + 双站点实战 + SELinux 排错
运维·自动化运维
极小狐2 天前
干掉流水线里的长期密钥:用 OIDC 让 CI/CD 与云以临时凭证对接
microsoft·ci/cd·gitlab·devops·ci·cd·oidc