Just 工程手册:把团队命令收敛为透明、可移植的 Recipe
仓库里的命令常常没有技术难度,却有很高的沟通成本:有人记得 docker compose 的参数,有人从 Wiki 复制旧命令,有人在错误目录执行迁移,还有人在 Windows 上遇到 Bash 语法失败。把它们写进一个 justfile,团队就能用 just test、just dev 和 just clean 找到统一入口,同时仍看得见底层命令。
Just 的官方定位是 command runner。它擅长 recipe、参数、变量、依赖和命令发现,不负责像构建系统那样推导文件依赖图、增量产物、远端缓存或分布式执行。需要这些能力时,应保留 Maven、Gradle、CMake、Bazel 等权威构建工具,让 Just 只做薄入口。
安装并核对命令来源
Windows、macOS 和常见 Linux 发行版都有包管理入口:
winget install --id Casey.Just --exact
just --version
Get-Command just | Format-List Source,Versionbrew install just
just --version
command -v justsudo apt install just
just --version发行版仓库版本可能落后于上游。安装前到 Just Packages核对当前平台命令与维护来源;升级前用 just --version 对照项目允许版本和 Just Releases。团队应固定来源和版本范围,保留上一版二进制或包缓存,避免开发机与 CI 使用不同语法能力。
用成功和失败两条 recipe 建立最小合同
在可删除的实验目录创建 justfile:
# 列表中显示的说明
hello:
echo "hello from just"
just --version
fail:
exit 23执行:
just --list
just hello
just failjust --list 应显示可调用 recipe,hello 打印命令和输出,fail 返回非零退出码。Just 默认在执行前回显命令,这有利于排障;命令前加 @ 可以隐藏该行回显,但不等于保护秘密,子命令和错误日志仍可能输出敏感值。
不带参数运行 just 时,默认执行文件中第一条 recipe。团队仓库更适合把第一条定义为安全的帮助或验证入口,避免默认触发部署、清理或发布:
default:
@just --listJust 会从当前目录向上寻找 justfile。这种便利也会造成“在错误仓库执行父目录命令”的风险;破坏性 recipe 要验证仓库标识、目标目录和环境,不依赖搜索行为兜底。
Recipe 是命令入口,不是产物规则
下面的 verify 依赖 lint 和 test:
lint:
npm run lint
test:
npm test -- --run
verify: lint test
npm run build依赖 recipe 先执行,同一调用链中可避免重复执行相同依赖。这里表达的是动作顺序,不是“当源码比产物新时才构建”。Just 不会根据输入输出自动判断最新状态;若需要增量构建,应由底层构建工具或专门任务图负责。
把底层命令保留下来还有一个好处:失败时开发者能直接复制 npm test -- --run 复现。若 recipe 包含十几层嵌套、动态拼接和隐藏输出,统一入口反而会遮住真实故障。
参数要有类型感和允许范围
Recipe 可以声明必填参数和默认值:
test suite="unit":
npm test -- --run --project "{{suite}}"
serve port="8080":
node server.mjs --port "{{port}}"调用:
just test
just test integration
just serve 9090插值最终会进入 Shell 命令字符串。引号可以降低空格拆分风险,却不能把任意用户输入变成安全参数。环境名、目标服务和操作类型应做允许列表校验:
deploy environment:
#!/usr/bin/env python3
import subprocess
import sys
allowed = {"dev", "staging"}
environment = "{{environment}}"
if environment not in allowed:
raise SystemExit(f"unsupported environment: {environment}")
subprocess.run(
["deploy-cli", "apply", "--environment", environment],
check=True,
shell=False,
)当参数会进入文件删除、数据库迁移、远程主机或 Shell 元字符语境时,使用参数数组和 shell=False 的脚本比复杂 recipe 更容易审查。生产环境发布不应只依赖一个字符串参数,还需要身份、审批、审计和二次确认。
Just 变量与环境变量不是同一种东西
Just 变量在解析和插值阶段使用:
app := "example-api"
dist := "dist"
build profile="debug":
cargo build --profile "{{profile}}"
@echo "artifact owner: {{app}}, directory: {{dist}}"调用者可以在命令行覆盖允许的变量:
just app=example-worker build release
just --set app example-worker build release环境变量则属于子进程边界。需要从运行环境读取时,明确校验是否存在,不要给生产凭证设置危险默认值:
registry-check:
#!/usr/bin/env python3
import os
import subprocess
if not os.environ.get("REGISTRY_TOKEN"):
raise SystemExit("REGISTRY_TOKEN is required")
subprocess.run(["registry-cli", "ping"], check=True)秘密值不得作为命令行参数或插值内容出现在回显中。优先让子进程从环境、标准输入或批准的凭证文件读取,并限制权限、生命周期和日志留存。
settings 会改变整个文件的解释方式
set 不是局部注释,而是执行语义:
set shell := ["bash", "-euc"]
set dotenv-required
check:
echo "checking"
npm run lintShell、dotenv、导出规则、重复定义和工作目录相关设置都会改变 recipe 行为。设置应集中放在文件前部,代码评审时把它们当作运行时配置。可用项和默认值从 Just settings核对,升级时重点审查弃用项和默认行为变化。
不要为了在 Windows 运行就假定所有机器都有 Bash。团队可以选择“统一容器或开发环境提供 Bash”,也可以为平台拆分脚本;关键是把依赖写进环境基线,而不是让失败发生在 recipe 中间。
每一行默认进入独立 Shell
普通 recipe 的每一行通常由独立 Shell 调用,因此状态不会自然跨行保留:
wrong:
cd services/api
pwd第二行未必在 services/api。更稳妥的写法是使用 recipe 级工作目录属性或把命令放在同一行:
[working-directory: 'services/api']
test-api:
go test ./...test-api-alt:
cd services/api && go test ./...工作目录影响相对路径、dotenv 搜索、导入文件和清理目标。排障时打印 pwd 或平台等价命令,并确认实际找到的 justfile:
just --show test-api
just --dump不要让清理 recipe 从任意父目录向下递归删除。先解析仓库根,确认目标位于允许目录,再执行删除;CI 中使用临时工作区可以进一步缩小风险。
Shebang Recipe 适合承载跨多行逻辑
复杂条件放在 shebang recipe 中,可以由明确解释器一次执行:
verify-artifact path:
#!/usr/bin/env python3
from pathlib import Path
import sys
artifact = Path("{{path}}").resolve()
root = Path.cwd().resolve()
if root not in artifact.parents:
raise SystemExit("artifact is outside repository")
if not artifact.is_file() or artifact.stat().st_size == 0:
raise SystemExit("artifact is missing or empty")
print(f"verified: {artifact.name}")这种方式让参数验证、异常和退出码处在一种语言里,也避免多行 Shell 状态丢失。代价是解释器必须进入开发机和 CI 基线;/usr/bin/env 在不同平台的可用性也要验证。Windows 原生团队可选择 PowerShell 脚本文件,并由 recipe 显式调用。
dotenv 是便利入口,也是秘密边界
开启 dotenv 加载后,Just 会把文件中的值作为环境变量提供给 recipe:
set dotenv-load
config-check:
node scripts/check-config.mjs仓库只提交 .env.example,真实 .env 加入忽略规则,并且不在 recipe 中打印完整环境。dotenv-override 会让文件值覆盖调用环境,可能悄悄替换 CI 注入的安全配置;没有明确理由时不要开启。
dotenv-path、dotenv-filename 与向上搜索行为不同,模块也可能加载各自环境文件。详细语义应从 dotenv settings核对。团队需要验证三种情况:文件不存在时是否按预期失败、CI 注入是否拥有正确优先级、子模块是否意外覆盖父模块的变量。
秘密管理器输出环境的能力也要限制:解密命令失败必须阻止 recipe,标准输出不得混入日志,临时文件要有严格权限并在退出时清理。Just 只负责调用,不替代凭证签发、轮换和撤销系统。
import 与 module 解决的是两种组织问题
import 把另一个文件的定义合并到当前命名空间:
import 'recipes/common.just'这适合少量、同一所有权的共享定义,但重复 recipe 或变量的覆盖顺序会增加阅读成本。可选 import 还可能让不同机器加载不同能力,CI 基线不应依赖“有文件就启用”的隐式行为。具体覆盖规则见 Just imports。
mod 创建独立命名空间:
mod api 'services/api/justfile'
mod web 'apps/web/justfile'
verify:
just api::test
just web::test调用:
just api test
just web::test模块拥有自己的 settings,变量和 recipe 不会自动跨模块共享。对多模块仓库,这种隔离通常比大规模 import 更清晰。模块能力和稳定状态应从 Just modules核对,并通过模块路径检查可发现性:
just --list api
just --list weblist、show、dry-run 与 choose 服务不同场景
命令入口首先要能被发现:
just --list
just --show verify
just --dump
just --dry-run verify--list 面向日常使用,--show 查看单条 recipe,--dump 适合审查解析后的整体定义,--dry-run 展示将执行的命令。dry-run 不是安全沙箱:解析阶段的变量求值、外部函数或版本差异仍需审查,升级时尤其要检查 release notes 对 dry-run 行为的修复。
just --choose 可通过选择器交互运行 recipe,但 CI、自动化和审计链路必须使用明确名称,不能依赖交互列表。团队帮助入口应描述风险和参数,不要把危险 recipe 与日常检查混在同一默认列表。
跨平台策略要先选一种,再验证
可行策略通常有三种:
所有平台进入同一个容器、Dev Container 或受控 Shell。Just 只调用跨平台脚本,例如 Node、Python 或 Go 程序。显式维护 Windows 与 POSIX recipe,入口再按平台选择。
第三种可以使用 os() 等能力做判断,但平台分支越多,代表性平台组合越重要。至少验证引号、路径、换行、环境变量、glob、可执行权限、信号和退出码。不要在 Linux CI 通过后就宣布 Windows 可用。
当前发布中若出现 Shell setting 弃用或替代能力,应先在测试分支运行 just --fmt --check、just --dump 和关键 recipe,再升级团队基线。动态能力以 Just 手册和 release notes 为准。
CI 与开发机使用同名 Recipe,不使用同一份秘密
CI 只安装固定版本并调用权威入口:
- name: Show just version
run: just --version
- name: Verify repository
run: just ci
env:
CI: "true"ci: lint test build verify-artifacts本机和 CI 的命令合同相同,权限边界不同:CI 使用只读 checkout、最小凭证、无交互模式、超时和干净工作区;发布 recipe 还要限制分支、环境与身份。失败日志应包含 recipe、底层命令、工作目录和工具版本,但不能打印秘密。
在流水线接入前,故意让 lint 失败、让参数非法、让 dotenv 缺失、让子进程超时,确认退出码和清理行为。只验证绿色路径,无法证明 command runner 能承担团队入口。
常见失败要沿解析链定位
just 找不到 recipe
先运行 just --list 和 just --dump,确认加载的是哪个 justfile,recipe 是否在 module 中、是否为私有定义、导入是否可选或名称是否冲突。不要复制同名 recipe 到根文件绕过命名空间。
参数里有空格或特殊字符就失败
检查参数经过了 Just 插值、Shell 解析还是脚本参数数组。复杂输入不要直接插入命令字符串;移动到脚本中校验并以参数数组调用底层进程。文件路径还要验证是否越过仓库根。
本机成功,Windows 或 CI 失败
打印 just --version、实际 Shell、工作目录和解释器来源。常见原因是 Bash 不存在、平台包版本过旧、行尾影响 shebang、全局工具未声明、dotenv 搜索路径不同或导入文件大小写不一致。
Recipe 显示成功,后台服务仍在运行
避免用 &、start 或脱离父进程的脚本启动服务。长进程保持前台,由专用进程管理器处理并发和信号。停止后检查端口、进程、容器和临时文件;CI 取消时也要运行清理步骤。
--dry-run 看起来安全,实际仍有副作用风险
Dry-run 只说明 Just 不执行计划中的 recipe 行,不代表所有解析能力在所有版本都无副作用。变量中的命令求值、dotenv 解密和导入链仍要单独审查。对生产操作,使用明确的 plan/apply 两阶段工具,而不是把 dry-run 当审批机制。
从旧脚本迁移时保留透明度
迁移 Package Scripts、Makefile 或散落 Shell 时,先挑一个只读检查入口:
记录旧命令的工作目录、Shell、参数、环境和退出码。新 recipe 先原样调用旧命令,不同时重写业务逻辑。在 Windows、POSIX 和 CI 比较输出与失败行为。
再把重复参数和目录收敛为变量或 module。删除旧入口前保留可恢复提交和使用统计。
若迁移后必须安装额外 Shell、隐藏了底层命令、破坏了退出码、扩大了凭证可见范围或让清理更危险,应停止迁移。Just 的价值是降低认知成本,不是用一门新 DSL 包住所有工程逻辑。
命令插值是最直接的注入面
任何来自用户、分支名、文件名或 CI 参数的值,只要进入 Shell 字符串,就要假定包含空格、引号和控制符。允许列表、参数数组和 shell=False 比“再加一层引号”可靠。
dotenv 会把本地便利带进 CI 风险
向上搜索、override 和 module 继承可能让同名变量取到意外值。生产凭证不落 dotenv;CI 明确注入并验证来源,日志和制品做秘密扫描,泄露后有撤销路径。
import 覆盖会制造隐式行为
深层 import 和重复定义让最终 recipe 难以预测。跨团队边界优先使用 module;共享文件固定所有权和变更评审,使用 --dump 对升级前后结果做差异检查。
Command Runner 不能承担构建正确性
Just 不追踪文件输入输出,也不提供完整构建缓存模型。把增量、产物图和远端缓存交给权威构建系统;Just 只暴露稳定入口、参数和环境合同。
简单工具也需要退出机制
团队模板要记录版本、owner、升级窗口、回滚二进制和迁移出口。若 recipe 数量持续膨胀、平台分支重复、执行时间依赖手写缓存,说明职责已越过 command runner,应拆分脚本或引入更合适的任务图。
just --version、二进制来源和团队允许版本可追溯。默认 recipe 安全,不会部署、发布或删除资源。每条常用 recipe 都有说明、参数、失败退出码和清理方式。
参数经过允许列表或脚本参数数组,不直接拼接危险 Shell。Just 变量、环境变量和 dotenv 的优先级清楚。Shell 与工作目录显式,Windows、POSIX、CI 有代表性验证。
shebang 解释器进入环境基线,行尾与可执行权限已验证。import 和 module 的所有权、覆盖与命名空间可解释。--list、--show、--dump 和 dry-run 能提供排障证据。
CI 只调用同名权威 recipe,秘密按最小权限注入。停止、超时和取消后没有残留进程、端口或容器。Just 没有重写底层构建图,升级与退出都有回滚路径。
