【Bug已解决】CI fails for code quality check: No such file or directory: 'ruff' 解决方案
一、现象长什么样
仓库的 CI 里有一条代码质量检查(lint),调用ruff做静态检查。某次改动后,这条 CI 直接挂:
/bin/sh: 1: ruff: not found No such file or directory: 'ruff'或者更隐蔽地(用subprocess.run(["ruff", ...])时):
FileNotFoundError: [Errno 2] No such file or directory: 'ruff'现象特征:
- 只在 CI 失败,本地
ruff跑得好好的——因为本地你用pipx/pip install ruff装过,PATH 里有;CI 的干净环境里没装; - 有时是"偶发":CI 用了缓存或某个 base image 里刚好有 ruff 的旧版本,换了 runner 后又没了;
- 报错发生在"执行 ruff"这一步,说明调用命令本身没问题,是运行环境里找不到这个可执行文件。
这是典型的"把 CLI 工具当成环境自带"的 CI 配置缺漏:脚本假定ruff一定在 PATH 上,但没保证它真的被安装。
二、背景
ruff是 Python 的 lint/格式化工具,有两种常见调用方式:
- 直接当可执行:
ruff check ./ruff format --check .——要求ruff在 PATH 上; - 作为 Python 模块:
python -m ruff check .——要求ruff作为包被安装到当前 Python 环境。
CI 里常见两种失败原因:
- 依赖没装:CI 的
requirements/pyproject的 dev 依赖里没列ruff,runner 干净环境就没有它; - 用了可执行但装成了模块:只
pip install ruff(装成模块,python -m ruff可用),脚本却写ruff(依赖 PATH 上的可执行),而pip install不一定把可执行链接进 PATH(取决于环境/隔离),于是ruff找不到; - 版本漂移:本地 ruff 新版,CI 旧版或反之,规则不一致,甚至某些规则名变了导致"找不到规则"式报错——但本题是更直接的"文件不存在"。
核心问题:CI 把ruff的存在当成前提,却没在 CI 流程里显式保证它存在。
三、根因
根因一句话:CI 的代码质量检查脚本直接调用ruff可执行文件,但 CI 环境没有安装/暴露ruff(既没列进依赖,也没用python -m ruff这种"依赖已安装包"的方式调用),导致执行时FileNotFoundError: ruff。
具体:
- 依赖缺口:
ruff不在 CI 安装的依赖清单里(或只在本地 dev 环境,没进 CI 用的 lockfile); - 调用方式脆弱:脚本写死
ruff ...依赖 PATH 可执行,而不是python -m ruff ...(后者只要包装了就可用,不依赖 PATH 链接); - 环境不可复现:本地有、CI 没有,因为本地和 CI 的依赖来源不同步;
- 报错早、阻塞大:lint 是门禁,ruff 找不到直接整条质量检查红,连带可能阻断合并。
本质是"工具可用性没在 CI 里被显式保证"。
四、最小可运行复现
下面用纯 Python 模拟"可执行不存在"的两种调用方式的差异:
import subprocess import sys def run_as_executable(): """旧方式:直接调 ruff 可执行,依赖 PATH。""" try: return subprocess.run(["ruff", "--version"], check=True, capture_output=True) except FileNotFoundError as e: return f"失败: {e}" def run_as_module(): """更稳方式:python -m ruff,依赖包已安装。""" try: return subprocess.run([sys.executable, "-m", "ruff", "--version"], check=True, capture_output=True) except FileNotFoundError as e: return f"失败: {e}(说明包也没装)" def demo(): print("直接可执行:", run_as_executable()) print("python -m:", run_as_module()) if __name__ == "__main__": demo()如果环境里ruff没装:第一行抛FileNotFoundError: ruff(和线上一致);第二行如果包装了也会成功,没装才失败。复现了"直接调可执行在干净环境里找不到"的核心问题,也提示了修复方向——用python -m ruff或确保安装。
五、解决方案(第一层):保证 CI 安装 ruff(进依赖清单)
第一层最稳妥:把ruff显式加进 CI 用的依赖,保证它一定存在:
# pyproject.toml 的 dev 依赖(或 requirements-dev.txt) [project.optional-dependencies] dev = [ "ruff>=0.5.0", # 显式固定下限,避免版本漂移 ]CI 步骤里先装:
# .github/workflows/quality.yml - name: Install dev deps run: pip install -e ".[dev]" # 或 pip install ruff - name: Lint run: ruff check .或者最小改动,单独装:
- name: Install ruff run: pip install "ruff>=0.5.0" - name: Lint run: ruff check .关键是:ruff 不再依赖"本地恰好装过",而是 CI 自己安装,环境可复现。
六、解决方案(第二层):改用python -m ruff,去掉 PATH 依赖
第一层保证了"包被安装",第二层再让调用方式不依赖 PATH 链接,更稳:
# 用 python -m ruff,只要包装了就能调,不要求可执行在 PATH - name: Lint run: python -m ruff check . - name: Format check run: python -m ruff format --check .同时在项目脚本里也统一:
import subprocess, sys def run_ruff(path="."): # 统一用 python -m ruff,避免 PATH 上找不到可执行 return subprocess.run([sys.executable, "-m", "ruff", "check", path], check=True) if __name__ == "__main__": run_ruff()python -m ruff的好处:它解析的是"当前sys.executable对应的环境里装的 ruff 包",只要pip install ruff进了这个环境就 100% 可用,不受 PATH 链接、venv 激活状态影响。配合第一层的显式安装,双重保险。
七、解决方案(第三层):CI 预检 + 锁版本 + 缓存
第三层加健壮性,避免"偶发找不到"和"版本漂移":
# 1) 锁版本,避免本地新/CI 旧导致规则不一致 - name: Install pinned ruff run: pip install "ruff==0.6.9" # 2) 预检:真正执行前先确认可用,给出友好报错 - name: Preflight ruff run: python -m ruff --version # 3) 缓存 pip,加速且避免偶发网络失败导致没装上 - uses: actions/setup-python@v5 with: python-version: "3.11" cache: pip本地也加个Makefile/tox统一入口,保证"本地跑的和 CI 跑的是同一套":
.PHONY: lint lint: python -m ruff check . python -m ruff format --check .这样开发者本地make lint与 CI 调用完全一致,本地能过 CI 必过(在同样版本下),消除"本地绿 CI 红"的落差。
八、落地建议
如果你在 CI 遇到 "No such file or directory: 'ruff'",建议:
- 加依赖:把
ruff显式写进 dev 依赖 / requirements,固定版本下限。 - 改调用:脚本和 CI 都用
python -m ruff,去掉 PATH 依赖。 - 锁版本:固定
ruff==x.y.z,避免规则漂移。 - 加缓存:pip 缓存加速,避免偶发安装失败。
- 统一入口:
Makefile/tox让本地与 CI 一致。 - 预检:CI 先
python -m ruff --version确认可用。
九、排查清单
如果 CI 报 "No such file or directory: 'ruff'",按顺序查:
- 确认 ruff 是否在 CI 安装的依赖里:没有就加进 dev 依赖/requirements。
- 看调用方式:脚本是
ruff还是python -m ruff?前者依赖 PATH,改后者更稳。 - 确认 CI 装了依赖:pip install 步骤有没有包含 ruff,或单独
pip install ruff。 - 锁版本:本地与 CI ruff 版本是否一致,规则差异会导致误报。
- 加预检:CI 先
python -m ruff --version, early fail。 - 本地复现:用干净 venv
python -m ruff check .,确认本地和 CI 一致。 - 看缓存/网络:偶发失败可能是安装阶段网络问题,加 pip 缓存。
十、小结
CI 报No such file or directory: 'ruff',根因是代码质量检查脚本直接调用ruff可执行文件,但 CI 的干净环境里既没有把ruff装进依赖清单,也没有用python -m ruff这种"依赖已安装包"的方式调用,于是执行时找不到这个可执行文件。它只在 CI 暴露(本地你装过所以有),属于"工具可用性没在 CI 里被显式保证"的配置缺漏。
修复分三层:第一层把ruff显式加进 CI 依赖并固定版本下限,保证一定被安装;第二层把调用统一改成python -m ruff,去掉对 PATH 上可执行链接的依赖,只要包装了就可用;第三层加版本锁定、pip 缓存和make lint统一入口,消除"本地绿 CI 红"的版本/环境落差,并加预检 early fail。核心心法是:CI 里调用的任何 CLI 工具,都不能假设它"环境自带"——必须显式安装(进依赖)、并用不依赖 PATH 的方式调用(python -m),才能保证环境可复现、构建稳定。