M003:读懂有效配置:层级、覆盖与团队边界
目标:先预测、再验证 global、parent、project 与 local 配置如何共同产生最终值。
0–7 分钟:最终配置不是某一个文件
mise 会从当前目录向上发现配置,并把较广范围的值与更具体目录的值合并。更靠近当前目录的配置可以覆盖父目录中的同名键。[S03]
先记住两种不同的合并方式:
[tools]、[env]这类键值配置可以按键继承与覆盖。例如父目录有 Python,子目录增加 Node,最终可能同时存在。- 同名 task 通常是一个完整定义;子目录重写
tasks.test时,不要假设父任务中的字段会自动递归保留。
mise.local.toml 处于很高优先级,设计用途是本机、不提交的覆盖。它适合个人路径和临时偏好,不适合团队必须拥有的工具或任务。[S03]
7–20 分钟:做一个可重复的三层实验
这次不用猜你的真实 global 配置。我们创建 parent、project、local 三层,并在执行前写出预测。
步骤 1:创建目录结构
rm -rf /tmp/mise-config-lab
mkdir -p /tmp/mise-config-lab/project
cd /tmp/mise-config-lab
创建 parent 配置:
cat > mise.toml <<'EOF'
[tools]
python = "3.11"
[env]
ENV_SOURCE = "parent"
PARENT_ONLY = "yes"
[tasks.describe]
run = 'printf "task=parent ENV_SOURCE=%s\n" "$ENV_SOURCE"'
EOF
创建 project 配置:
cat > project/mise.toml <<'EOF'
[tools]
python = "3.12"
node = "22"
[env]
ENV_SOURCE = "project"
PROJECT_ONLY = "yes"
[tasks.describe]
run = 'printf "task=project ENV_SOURCE=%s\n" "$ENV_SOURCE"'
EOF
创建只在本机存在的 local 覆盖:
cat > project/mise.local.toml <<'EOF'
[env]
ENV_SOURCE = "local"
EOF
printf 'mise.local.toml\n' > project/.gitignore
cd project
步骤 2:运行前先预测
不要先执行命令。复制并填写:
| 项目 | 我的预测 | 理由 |
|---|---|---|
| Python | parent 还是 project 更具体? | |
| Node | 哪一层声明了它? | |
PARENT_ONLY | 未冲突的键会怎样? | |
PROJECT_ONLY | 哪一层提供? | |
ENV_SOURCE | local 的优先级如何? | |
describe 输出 | 同名 task 取哪一个完整定义? |
步骤 3:查看“加载了谁”
mise config
预期观察:输出包含 parent、project 和 local 配置,且能看出优先级顺序。若只看到一个文件,先检查当前目录和文件名,不要直接修改结论。
步骤 4:查看最终 tools 与 env
mise ls --current
mise exec -- sh -c 'printf "ENV_SOURCE=%s PARENT_ONLY=%s PROJECT_ONLY=%s\n" "$ENV_SOURCE" "$PARENT_ONLY" "$PROJECT_ONLY"'
预期结果:Python 由 project 覆盖为 3.12 选择器,Node 来自 project;两个 *_ONLY 都保留;ENV_SOURCE 最终为 local。
步骤 5:验证 task 的替换边界
mise run describe
预期打印 task=project ENV_SOURCE=local。这同时说明两件事:同名 task 使用 project 定义,而运行环境仍是合并后的最终 env。
步骤 6:做一次反事实验证
mv mise.local.toml mise.local.toml.disabled
mise exec -- sh -c 'printf "ENV_SOURCE=%s\n" "$ENV_SOURCE"'
mv mise.local.toml.disabled mise.local.toml
预期值从 local 退回 project。这一步比只看最终输出更强,因为它证明了覆盖来自哪一层。
20–26 分钟:把“值”追溯到“所有者”
现在为每个有效值画一条来源链:
Python: parent 3.11 → project 3.12(最终)
ENV_SOURCE: parent → project → local(最终)
PARENT_ONLY: parent(保留)
describe task: parent 定义 → project 整体替换(最终)
再用下面规则决定文件归属:
| 内容 | 推荐位置 | 原因 |
|---|---|---|
| 团队统一工具、任务、非敏感默认值 | 提交的 mise.toml | fresh checkout 必须恢复 |
| 开发者个人绝对路径、临时覆盖 | 被忽略的 mise.local.toml | 不应强加给团队 |
| 真实 secret | 外部 secret store 或被忽略来源 | 不进入 Git 历史 |
| 项目要求的最低 mise 能力 | 提交配置中的 min_version | 让不兼容尽早失败 |
常见失败与下一步:
- 预测与结果不同:先看
mise config的实际文件列表,再检查是否有真实 global 或更高优先级环境配置。 - local 文件被 Git 跟踪:仅添加
.gitignore不能移除已经跟踪的文件;先确认没有 secret,再按团队流程从索引移除。 - 子任务丢失父任务字段:这是把 task 当递归 map 合并的典型错误;在子层写出完整 task 定义。
- 只保存
mise.toml截图:它看不到 parent/global/local;同时保存加载列表和最终值。
26–30 分钟:检索问题
问题:为什么只贴当前目录的 mise.toml 不能解释所有有效值?
作答后查看参考答案
mise 会发现并合并 global、父目录、项目、环境专用和 local 等配置。最终值可能继承自更广层,也可能被更具体层覆盖;必须同时查看加载文件、合并规则和最终输出。
情境题:一位开发者需要把本机 SDK 路径改成 $HOME/sdk。应放在共享 mise.toml 还是 mise.local.toml?
作答后查看参考答案
通常放在被忽略的 mise.local.toml,因为绝对个人路径无法被其他机器复用。若团队确实需要统一 SDK,应提交可移植的工具声明或相对路径,而不是提交某人的主目录。
通过标准
- 在执行前完成覆盖预测,并用实际输出逐项核对。
- 能区分 tools/env 的按键合并与同名 task 的整体替换。
- 能为至少三个最终值指出来源链。
- 共享配置、个人覆盖与 secret 的边界清楚。