Task 工程手册:让多语言项目的任务图可验证、可停止、可治理
一个仓库同时包含 Go 服务、前端应用、数据库迁移和容器环境时,团队通常会积累 build.ps1、dev.sh、Makefile 与 CI YAML 四套入口。真正危险的不是命令多,而是依赖顺序、工作目录、跳过条件和停止动作各不相同:本机说“已经最新”,CI 却重新构建;父任务失败,后台服务仍占着端口;新成员不知道哪条脚本才是权威入口。
Task 用 YAML 描述命令、任务依赖、变量和状态判断,适合给多语言仓库建立一层薄而明确的命令合同。它不是 Maven、Gradle、CMake 或 Bazel 的替代品,也不会凭空理解源码依赖。Taskfile 应负责编排权威底层命令,而不是重写另一套构建系统。
安装后先确认二进制来自哪里
Windows 可以使用 WinGet,macOS 或 Linux 可以使用 Homebrew:
winget install Task.Task
task --version
Get-Command task | Format-List Source,Versionbrew install go-task
task --version
command -v task企业内网更适合从批准的软件源分发固定版本二进制。下载发布包后,应对照发布页提供的 task_checksums.txt 校验 SHA-256,再把二进制放入受控目录。完整安装方式和包维护归属可从 Task 安装说明核对。
不要让开发机跟随 latest、CI 固定旧版。团队至少记录:允许的 Task 版本、下载来源、校验值、升级责任人和回退包。需要判断当前稳定版本时,使用 task --version 查看本机,再到 Task Releases检查目标版本的变更和资产,不把版本号散落在多个脚本里。
从一个会失败的最小任务开始
在可删除的实验目录创建 Taskfile.yml:
version: '3'
tasks:
hello:
desc: 打印运行环境
cmds:
- task --version
- echo "hello from Task"
fail:
desc: 验证失败退出码
cmds:
- cmd: exit 23
platforms: [linux, darwin]
- cmd: powershell -NoProfile -Command "exit 23"
platforms: [windows]执行:
task --list
task hello
task failhello 应显示版本与文本,fail 必须返回非零退出码。这个反向实验比“打印成功”更重要:只有底层失败能传到 Task,再传到 CI,任务入口才可以承担质量判断。
version 描述的是 Taskfile schema 需求,不是文档装饰。version: '3' 使用 v3 schema;项目依赖某个较新的语法能力时,可以写满足 SemVer 的最低 CLI 版本。运行器过旧应直接报错,而不是悄悄忽略字段。具体字段以 Taskfile schema和 Taskfile 版本语义为准。
任务图只表达编排关系
deps 表示当前任务开始前需要完成的任务:
version: '3'
tasks:
lint:
cmds:
- npm run lint
test:api:
dir: services/api
cmds:
- go test ./...
test:web:
dir: apps/web
cmds:
- npm test -- --run
verify:
desc: 执行提交前验证
deps: [lint, test:api, test:web]
cmds:
- echo "all checks passed"依赖任务可以并发执行,所以不能让 test:web 暗中依赖 lint 生成文件。存在先后关系时,应显式建立链路,或者在当前任务的 cmds 中顺序调用:
tasks:
generate:
cmds:
- npm run generate
build:
deps: [generate]
cmds:
- npm run build
package:
cmds:
- task: build
- docker build -t example/app:local .Task 负责调度,真正的源码依赖、编译缓存与产物正确性仍由语言构建工具负责。若任务图开始逐文件描述编译关系,或者团队需要远端缓存、受影响范围计算和分布式执行,应转向专门的构建图工具。
sources、generates 与 status 决定“是否需要执行”
最容易被误用的能力是增量跳过。下面任务根据输入与输出判断是否最新:
version: '3'
tasks:
docs:build:
sources:
- docs/**/*.md
- package.json
- pnpm-lock.yaml
generates:
- dist/**/*
cmds:
- pnpm docs:buildTask 默认可基于校验和判断状态,也可以配置时间戳策略。关键问题不是选哪个名字,而是输入集合是否完整:构建还依赖 Node 版本、环境变量、远程 schema 或容器镜像时,只列源码文件会产生错误命中。
外部状态适合用 status 检查:
tasks:
image:local:
status:
- docker image inspect example/app:local >/dev/null 2>&1
cmds:
- docker build -t example/app:local .status 中所有命令成功时,任务会被视为最新。这里要故意测试两条路径:删除镜像后任务应执行;镜像存在时应跳过。若状态命令访问网络、读取当前集群或依赖登录态,结果会随环境漂移,不适合作为可复现构建的唯一依据。
治理增量状态时,把每个输出对应的完整输入、工具版本和环境维度写清。升级 Task 或调整 glob 后,先清理 .task 状态目录与目标产物做一次冷构建,再比较两次构建结果,防止旧指纹掩盖缺失依赖。
变量用于参数化,环境变量用于进程边界
静态变量、动态变量和调用参数不能混成一个秘密容器:
version: '3'
vars:
APP_NAME: example-api
GIT_SHA:
sh: git rev-parse --short HEAD
tasks:
build:
requires:
vars: [TARGET]
env:
CGO_ENABLED: '0'
cmds:
- go build -trimpath -o "dist/{{.APP_NAME}}-{{.TARGET}}" ./cmd/api
vars:
OUTPUT: "dist/{{.APP_NAME}}-{{.TARGET}}"调用时显式传入:
task build TARGET=linux-amd64模板变量在 Task 展开阶段求值,env 进入子进程环境,Shell 自己还会做一次变量和引号解释。用户输入不得直接拼进 sh -c、eval、删除路径或发布命令。环境名称应使用允许列表验证,破坏性任务应增加确认与目标环境检查。
凭证只从批准的秘密管理入口注入,Taskfile 里保留变量名,不保留值:
tasks:
registry:check:
requires:
vars: [REGISTRY_HOST]
preconditions:
- sh: test -n "$REGISTRY_TOKEN"
msg: REGISTRY_TOKEN 未注入
cmds:
- registry-cli ping --host "{{.REGISTRY_HOST}}"不要在命令中 echo token,也不要因为开启详细日志就把整个环境打印出来。若当前 Task 版本支持秘密变量遮罩,也只能作为第二道防线;源头仍应遵循最小权限、短时凭证、日志脱敏与到期轮换。
includes 让目录自治,但不能隐藏所有权
多模块仓库可以分拆 Taskfile:
version: '3'
includes:
api:
taskfile: ./services/api/Taskfile.yml
dir: ./services/api
web:
taskfile: ./apps/web/Taskfile.yml
dir: ./apps/web
tasks:
verify:
deps: [api:test, web:test]命名空间让模块维护自己的命令,根 Taskfile 只暴露团队级合同。dir 要与模块实际根目录一致,避免被 include 的任务在错误目录写产物。共享片段应有 owner、兼容版本和升级记录;过度 flatten 会让任务重名和覆盖关系难以识别。
先运行 task --list-all 检查任务是否来自预期命名空间,再故意从仓库子目录调用,核对 Taskfile 搜索和工作目录行为。目录语义不清时,日志至少打印任务名、工作目录和底层工具版本。
平台差异应显式分支
同一条 Shell 字符串不可能自动跨平台。简单差异可用 platforms:
tasks:
clean:
cmds:
- cmd: rm -rf ./dist
platforms: [linux, darwin]
- cmd: powershell -NoProfile -Command "Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./dist"
platforms: [windows]删除任务仍应先把目标限制在仓库内,并在 CI 使用临时工作区。复杂逻辑更适合放进 Go、Node 或 Python 脚本,由 Task 只传参数和退出码。团队至少在 Windows 与一种 POSIX 环境验证:路径分隔符、引号、glob、环境变量、换行、可执行权限和信号停止。
run、watch 与长进程要验证停止语义
开发环境常见写法是让多个服务一起运行:
tasks:
dev:api:
dir: services/api
cmds:
- go run ./cmd/api
dev:web:
dir: apps/web
cmds:
- pnpm dev
dev:
deps: [dev:api, dev:web]这类任务不能只验证“都启动了”。按 Ctrl+C 后还要检查子进程、监听端口、容器和临时文件是否清理。CI 中要设置总超时,失败时收集子任务日志,并确保取消作业会向进程树传播终止信号。
task --watch 可以根据输入变化重新执行任务,但高频文件、生成目录和依赖缓存若进入监听集合,会形成自触发循环或资源风暴。先用小范围 sources 验证触发条件,再监控 CPU、文件句柄和重启频率;生产发布任务不应依赖 watch。
远程 Taskfile 是远程代码执行
远程 Taskfile 下载后会执行命令,安全等级应与外部安装脚本相同。Task 的远程能力会提示首次信任并记录校验和;内容变化后再次提示。非交互 CI 中使用 --yes 会放行所有提示,不能作为默认解决方案。
更稳妥的 include 同时固定不可变地址与 checksum:
version: '3'
includes:
shared:
taskfile: https://example.com/taskfiles/v1.4.2/Taskfile.yml
checksum: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef示例校验值不能直接使用。实际接入时先下载到隔离目录审查内容,确认所有 include、下载、Shell 和环境读取行为,再计算并记录校验值。企业 CA、mTLS 和 trusted host 的配置应依据 远程 Taskfile 安全说明实施;HTTP、自动信任与浮动分支地址不应进入 CI 基线。
远程内容变更应走依赖升级流程:提出新地址和 checksum、展示差异、在隔离环境运行、批准后合并。回滚就是恢复旧地址与旧 checksum,而不是在事故现场临时接受新的远程内容。
CI 只调用同一条权威入口
CI 不应复制 Taskfile 内部命令。流水线只负责安装固定版本、注入最小凭证、恢复受控缓存并调用任务:
- name: Verify Task version
run: task --version
- name: Run repository checks
run: task ci
env:
CI: "true"version: '3'
tasks:
ci:
deps: [lint, test, build]
cmds:
- task: artifacts:verify本机与 CI 使用同名入口,但环境边界可以不同:CI 必须禁用交互、固定超时、使用只读或短时凭证、从干净工作区开始,并上传失败证据。缓存命中不能代替冷构建;定期清空 Task 状态和工具缓存,证明任务没有遗漏输入。
按现象排查,不要先删所有缓存
任务没有执行
先运行带详细输出的任务并检查 status、sources、generates 和方法配置。删除一个明确的输出文件,任务仍跳过,通常说明 status 命令过宽或生成物 glob 指向错误目录。只清理对应任务状态,保留现场日志,避免一上来删除所有缓存后失去证据。
同一任务执行了两次
检查它是否同时作为多个依赖和显式命令调用,变量参数是否不同,以及动态变量是否使实例不再等价。重复执行若会写同一目录、抢同一端口或执行数据库迁移,应增加互斥、幂等或把副作用移动到唯一父任务。
本机成功,CI 找不到命令
打印 task --version、工作目录、PATH 和底层工具版本。常见原因是本机依赖全局安装、CI checkout 目录不同、include 的 dir 错误、私有源凭证未注入或 CA 未安装。修复依赖声明,不要在 CI 临时全局安装一个未知最新版遮住问题。
停止后端口仍被占用
检查 Task 启动的是前台进程、Shell 包装器还是后台命令。让服务保持前台运行,避免 &、start 等脱离父进程的方式;必要时使用能管理进程组的专用工具。停止后用平台命令核对端口和进程,再清理容器与临时目录。
迁移要保留一段双轨窗口
从 Makefile、Shell 或 Package Scripts 迁移时,先选一条无破坏性的 lint 或 test 链路:
记录旧入口的命令、目录、环境、退出码和产物。Taskfile 只调用旧权威命令,不立即重写逻辑。在代表性平台和 CI 比较输出、退出码与产物摘要。
再逐步迁移参数、依赖和状态判断。删除旧入口前,保留可恢复的提交和明确退场日期。
出现产物差异、错误被吞、冷构建失败、停止后残留进程或远程依赖不可追溯时,应停止迁移并回到旧入口。Taskfile 变短不是成功标准,行为等价和证据完整才是。
错误增量比重复构建更危险
重复构建浪费时间,错误跳过会发布旧产物。对发布链路,任何无法完整建模的外部输入都应让任务保守执行;每次升级状态策略后做冷构建与产物比对。
并发会放大共享资源冲突
deps 并发运行时,共享输出目录、数据库、固定端口和限流 API 都可能互相污染。任务图需要区分纯任务与副作用任务;副作用节点要幂等、可串行或使用隔离资源名。
远程 include 改变了供应链边界
浮动 URL、自动信任和未固定 checksum 等价于允许远端随时修改 CI 执行内容。远程 Taskfile 必须纳入依赖审查、来源准入、变更记录和紧急撤销。
任务入口可能成为秘密扩散器
动态变量、Shell tracing、summary、失败日志和子进程都可能输出凭证。秘密只按任务需要注入,默认关闭值回显,日志进入制品前做扫描,凭证泄露后同时撤销 token 与清理历史日志。
没有 owner 的 Taskfile 会变成第二套平台
共享任务需要 owner、兼容策略、升级窗口和退出路径。根 Taskfile 只保留跨模块合同,模块任务由模块团队维护;涉及发布、生产数据库和云资源的任务还要有审批与审计入口。
task --version、二进制来源和校验方式可追溯。Taskfile schema 与最低 CLI 能力匹配。成功、失败、跳过和重新执行四条路径都验证过。
deps 只表达真实依赖,并发任务没有共享资源竞争。sources、generates、status 覆盖完整输入,冷构建定期运行。变量不直接拼接危险 Shell,秘密不写入仓库和日志。
includes 有命名空间、目录、owner 与兼容边界。Windows、POSIX 与 CI 至少各有代表性验证。Ctrl+C、超时和 CI 取消后没有残留进程、端口或容器。
远程 Taskfile 固定来源与 checksum,变更经过审查。Task 只编排权威构建工具,不复制第二套依赖图。升级有灰度、回滚包、旧 Taskfile 验证和负责人。
