Insights·2026-07-28

把设计检查放进 CI 需要什么?——用退出码做成的关卡

关键是把设计评审从人的眼睛挪到退出码上。代码有 lint 把关,界面却每次都要有人看过才放行。Impeccable 的 detect 命令不调用 LLM、不需要 API 密钥,只跑规则检查,并用退出码报告结果:有问题为 2,没问题为 0。于是在 CI 工作流里写一行 npx impeccable detect src,不必安装专用 action 就成了关卡。检查方式随对象而异:HTML 会连同引用的 CSS 一起静态分析,JSX、CSS 之类的其他文件走模式匹配,给网址则用真实浏览器读渲染后的画面。所以源码扫描干净,网址扫描仍可能查出别的项目,把预览部署地址一并接上就能补上这段落差。日常够用的选项有四个:json、scope、viewport、no-advisory。需要例外时,用文件内的 impeccable-disable 注释就地豁免,或用 ignores add-value 记进仓库配置——两种方式都要写下理由,判断因此以记录的形式留存。

CI 설정과 터미널 화면 — 워크플로에 npx impeccable detect src 한 줄이 들어 있고, 로컬에서 같은 명령을 돌려 종료코드 2가 나오는 출력과 JSON 결과, 그리고 json·scope·viewport·no-advisory 옵션 표가 함께 있다.
지적이 있으면 종료코드 2로 끝나므로 워크플로 한 줄이 그대로 게이트가 된다

为什么只有界面每次都要过人手

代码质量早就有自动装置。lint 拦住违规,类型检查对齐形状,测试守住行为。这三样不用人盯着也会跑,违反了就合不进去。

界面不是这样。对比度有没有崩、卡片是不是套了两层、移动端文字会不会被截断,通常由评审者用眼睛判断。于是评审者忙的时候就过了,换个人标准也跟着变,同一条意见几个月后又冒出来。

开始把界面交给 AI 之后,这个问题会放大。人一天做出来的界面,代理一小时就能做出来,而评审仍然是人的速度。不把“可以自动检查的部分”和“确实需要人看的部分”分开,瓶颈就原封不动地留着。

机器能判断的范围比想象中宽:对比度、行高、可点区域大小、标题层级跳级、卡片嵌套、配色——凡是能用数值判定的都算。把这些下沉到 CI,人就可以专注在层级与含义上。

一个退出码就是整道关卡

Impeccable 的 detect 通过退出码报告结果:有问题以非零结束,没问题为 0。CI 把非零结束的命令视为该步骤失败,所以工作流里写一行命令,中间不需要任何 action 或插件,关卡就成立了。

实际跑一遍是这样:拿一张写满常见毛病的 HTML 去跑,得到 17 条、退出码 2;只修被指出的项目再跑一次,得到 0 条、退出码 0。流水线就靠这一个差别分岔。

值得接的位置有两处。一处以源码目录为对象,另一处以预览部署地址为对象。下一节会说明,这两者抓到的东西并不相同。

不调用 LLM 这一点在 CI 里尤其重要。不必把 API 密钥放进机密变量,同样的输入给出同样的结果,运行时间与成本都可预期。判定会随模型回答漂移的检查,是不能当关卡用的。

.github/workflows/design.yml
- name: Design check
  run: npx impeccable detect src/

# 想连预览部署一起检查
- name: Design check (deployed)
  run: npx impeccable detect ${{ steps.deploy.outputs.preview-url }}

对象决定检查方式

命令是同一条,但给它什么,内部做的事就不同。HTML 文件会连同引用的 CSS 一起做静态分析。JSX、TSX、CSS 之类的其他文件用模式匹配扫一遍。给网址则用真实浏览器把页面打开,看渲染完成后的画面。

这个差别正是实务中容易犯迷糊的地方。只扫组件源码,能抓到以数值呈现的项目,却会漏掉只有拼装之后才显现的项目。比如紧贴标题上方的小标签,或者最终的对比度值,都要等画面组装好才能判定。源码扫描报 0,线上地址却被指出,原因就在这里。

所以如果只接一道关卡,预览地址那边抓得更多。代价是速度:网址检查要启动浏览器,比源码扫描慢。每次推送跑源码扫描、预览就绪后再跑网址扫描,是比较稳妥的组合。

选项知道四个就够。json 是机器可读输出,适合生成报告或发到 PR 评论;scope 把检查限定在类型、布局这类范围;viewport 用于网址检查时改变宽度,以移动端再跑一遍;no-advisory 则直接隐藏被归类为参考性的发现。

常用写法
npx impeccable detect --json src/ > findings.json   # 生成报告
npx impeccable detect --scope type,layout src/      # 限定范围
npx impeccable detect --viewport 390x844 <URL>      # 以移动端宽度再跑
npx impeccable detect --no-advisory src/            # 隐藏参考性发现

例外连同理由留在代码里

设了关卡就一定会需要例外。品牌选定的字体可能正好在过度使用的名单上,或者某个页面刻意偏离规则。这时不要整体关掉检查,而是用两种方式记下来。

第一种是文件内注释。写 impeccable-disable,后面跟规则名和理由,就在那个文件里豁免;也有只豁免一行的写法。例外就贴在代码旁边,文件被移动或删除,例外也随之消失。

第二种是仓库配置。用 ignores 命令把规则、文件路径或某个具体取值登记为例外,会作用于整个仓库,并且可以一并写下理由,方便日后追溯为什么放行。

两种方式有个共同点:例外留在文件里,而不是留在对话里。评审中口头达成的例外传不到下一个人,写进仓库的例外能传下去。

记录例外
<!-- impeccable-disable overused-font -- 品牌指定字体 -->

/* impeccable-disable-line dark-glow */

npx impeccable ignores add-value overused-font Inter --reason "Brand font"
npx impeccable ignores list

想在提交之前就拦下来

CI 是最后一张网。等东西已经做好了才被拦下,就要付返工的代价。所以在受支持的工具上会一并装上编辑时刻运行的钩子:UI 文件被改动的瞬间跑检测器,把结果送回代理的流程里。

各工具介入的时点不同。Cursor 会在错误的写入落地之前拦住,其余工具则在修改完成之后提示。无论哪种,都是在人打开评审之前先过一道。

要不要用钩子,安装过程中会问。如果不喜欢自动化打断编辑流程,可以只接 CI。反过来,把大量界面工作交给代理的团队,从钩子上得到的回报更多——指摘立刻回来,代理才能在同一段上下文里改掉。