M005:统一入口:用 Tasks 编排本地检查
目标:把多个检查组织成可审阅的 mise run check,并证明失败码不会在编排中丢失。
0–6 分钟:task 的价值是统一执行契约
如果 README、开发者脚本和 CI 各写一套“差不多”的 lint/test 命令,三套入口会分别漂移。mise task 把命令、工具、环境、工作目录和依赖关系放进仓库,并在完整 mise 上下文中运行。[S05]
把 task 看成一个有接口的函数:
名称 + 输入(tools/env/files)+ 工作目录 + 依赖 + 命令 + 退出码
短而稳定的命令可以放在 TOML;复杂 shell 逻辑应放入可执行 file task,让脚本得到正常的语法高亮、lint 和测试,而不是塞进难维护的长字符串。[S05]
6–20 分钟:建立三个叶子任务和一个聚合任务
下面使用 Python 标准库,不要求额外 lint 包。
步骤 1:创建最小项目
rm -rf /tmp/mise-task-lab
mkdir -p /tmp/mise-task-lab/src /tmp/mise-task-lab/tests
cd /tmp/mise-task-lab
cat > src/calc.py <<'PY'
def add(left: int, right: int) -> int:
return left + right
PY
cat > tests/test_calc.py <<'PY'
import unittest
from src.calc import add
class CalcTest(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
if __name__ == "__main__":
unittest.main()
PY
步骤 2:声明工具、环境和任务图
cat > mise.toml <<'EOF'
[tools]
python = "3.12"
[env]
APP_ENV = "test"
[tasks.syntax]
description = "Compile Python sources"
run = "python -m compileall -q src tests"
[tasks.test]
description = "Run unit tests"
run = "python -m unittest discover -v"
[tasks.env-check]
description = "Check the task environment"
run = 'test "$APP_ENV" = test && printf "APP_ENV=%s\n" "$APP_ENV"'
[tasks.check]
description = "Run every local quality gate"
depends = ["syntax", "test", "env-check"]
EOF
这里 syntax、test、env-check 是叶子节点;check 只表达依赖,不复制三条命令。
步骤 3:先查看任务,再运行
mise tasks ls
mise run check
printf 'exit=%s\n' "$?"
预期观察:列表中能看到四个 task;三个叶子检查都执行,最终退出码为 0。依赖可能并行执行,所以日志先后顺序不应成为正确性条件。[S05]
步骤 4:确认 task 获得 mise 上下文
mise exec -- python --version
mise run env-check
预期 env-check 打印 APP_ENV=test。如果直接从普通 shell 运行 echo "$APP_ENV" 为空,也不矛盾:task 的环境由 mise 在执行时构造。
20–26 分钟:做一个真正的失败传播实验
“任务成功过一次”不能证明 CI 会正确失败。现在故意让测试失败,再检查聚合 task 的退出码。
步骤 1:保存原文件并制造失败
cp tests/test_calc.py /tmp/test_calc.py.good
python - <<'PY'
from pathlib import Path
p = Path("tests/test_calc.py")
p.write_text(p.read_text().replace("add(2, 3), 5", "add(2, 3), 6"))
PY
步骤 2:运行聚合入口
mise run check
status=$?
printf 'check_exit=%s\n' "$status"
预期观察:单元测试报告 5 != 6,聚合 task 返回非零。若 check_exit=0,检查是否使用了 || true、错误的 shell 管道、后台进程或一个从未运行测试的空 task。
步骤 3:恢复并再次验证
mv /tmp/test_calc.py.good tests/test_calc.py
mise run check
预期恢复绿色。保留“失败日志 → 非零退出码 → 修复后成功”的完整因果链,比只保存绿色截图更能证明任务图有效。
26–28 分钟:从子目录验证工作目录假设
cd tests
mise run env-check
mise run test
mise task 会提供 MISE_ORIGINAL_CWD、MISE_CONFIG_ROOT、MISE_PROJECT_ROOT 等上下文变量。[S05] 如需诊断,可临时加一个 task:
[tasks.where]
run = 'printf "original=%s\nconfig=%s\nproject=%s\n" "$MISE_ORIGINAL_CWD" "$MISE_CONFIG_ROOT" "$MISE_PROJECT_ROOT"'
解释时要区分:调用者从哪里发起、配置根在哪里、项目任务应以哪里为稳定根。不要让 task 偶然依赖“大家总在仓库根运行”。
常见失败与下一步:
No module named src:确认 task 的工作目录和 Python import 根;用wheretask 看上下文,不要硬编码某人的绝对路径。- 聚合 task 偶发失败:检查并行依赖是否偷偷共享临时文件或要求顺序;显式声明真实依赖。
- CI 运行的是复制出来的命令:删掉重复命令,让 CI 只调用
mise run check或mise run ci。 - 裸调用
mise check与内建命令冲突:自动化始终使用明确的mise run check。
28–30 分钟:检索问题
问题:为什么“本地脚本和 CI 各写一套相似命令”不够可复现?
作答后查看参考答案
两套入口会独立漂移,工具、环境、参数、工作目录和失败处理都可能不同。共享 mise run check 把执行契约集中在仓库中,让本地与 CI 调用同一个任务图。
通过标准
mise run check依赖至少三个有意义的叶子检查。- 有一次故意失败,且非零退出码传到聚合入口。
- 恢复后同一入口重新成功,没有用
|| true掩盖问题。 - 从子目录调用时,能解释原始目录、配置根与项目根。