Copier 总报错?一篇讲透排查、升级、治理和团队落地
如果你已经能跑copier copy,但一到check-update、update就反复踩坑,这通常不是工具本身不稳定,而是缺少一套可复用的工程闭环。本文把最核心的 5 个问题合并成一篇:最小闭环怎么跑、升级为什么失败、报错怎么排查、为什么要打 tag、如何接入团队 CI。
1. 先把最小闭环跑通
Copier 在团队里要稳定,最低闭环是:
copy -> 模板发布(tag) -> check-update -> update -> 验证提交基础检查:
copier-vgit--version最小命令:
copier copy ./my_copier_template ./destination-dproject_name=demo copier check-update ./destination copier update ./destination--defaults成功判定:
cd./destinationgitstatus2. 为什么升级经常失败
高频不是“命令拼错”,而是以下四类问题:
- 路径混用:
./destination和../destination在不同 cwd 下可能是两个目录。 - 引用丢失:
.copier-answers.yml缺少可追踪模板版本信息。 - 版本漂移:模板仓库没打 tag,更新来源不稳定。
- 变量断档:新增必填问题没有 default,也没在命令里
-d传值。
建议固定排查顺序:
路径 -> answers -> 版本(tag) -> 变量(default/-d)3. 报错场景与直接修复
场景 1:Cannot obtain old template references
- 先检查目标目录是否正确。
- 检查
.copier-answers.yml是否完整、是否被手工破坏。
场景 2:Question is required
- 在模板里给新变量加
default。 - 或执行更新时补参数:
-d key=value。
场景 3:更新后冲突
- 检查是否出现
.rej或冲突标记。 - 冲突必须人工处理,处理后再提交。
4. 模板仓库为什么必须打 tag
不打 tag 也许“偶尔可用”,但不适合团队长期维护。
打 tag 的价值:
- 可追踪:明确目标项目基于哪个模板版本。
- 可回滚:出问题时能回到稳定版本。
- 可协作:多人对版本语义有共识。
推荐发布动作:
gitadd.gitcommit-m"template: release v0.0.5"gittag v0.0.5gitpush --follow-tags5. 团队落地:培训 + CI 自动化并行
个人可用不等于团队可用。建议双线并行:
- 培训线:统一路径、统一版本规则、统一排障顺序。
- 自动化线:定时
check-update,有更新再update,有冲突就阻断。
脚本入口可以统一放在:
scripts/copier-update-check.sh
完整脚本如下(可直接复制保存为scripts/copier-update-check.sh):
#!/usr/bin/env bashset-euopipefailDESTINATION_PATH="./destination"CONFLICT="inline"PRERELEASES=falseSKIP_TASKS=falseCHECK_ONLY=falseDATA_FILE=""DATA_PAIRS=()usage(){cat<<'EOF' Usage: copier-update-check.sh [options] Options: --destination-path <path> Target project path (default: ./destination) -d, --data <key=value> Repeatable data pair for copier update --data-file <path> YAML/JSON data file for copier update --conflict <inline|rej> Conflict strategy (default: inline) --prereleases Include prerelease versions --skip-tasks Skip copier tasks during update --check-only Only check update availability -h, --help Show this help EOF}require_command(){localname="$1"if!command-v"$name">/dev/null2>&1;thenecho"ERROR: Required command not found:$name">&2exit1fi}run_copier(){set+e copier"$@"localcode=$?set-ereturn"$code"}while[[$#-gt0]];docase"$1"in--destination-path)DESTINATION_PATH="$2";shift2;;-d|--data)DATA_PAIRS+=("$2");shift2;;--data-file)DATA_FILE="$2";shift2;;--conflict)CONFLICT="$2";shift2;;--prereleases)PRERELEASES=true;shift;;--skip-tasks)SKIP_TASKS=true;shift;;--check-only)CHECK_ONLY=true;shift;;-h|--help)usage;exit0;;*)echo"ERROR: Unknown option:$1">&2;usage>&2;exit1;;esacdonerequire_command copier require_commandgit[[-d"$DESTINATION_PATH"]]||{echo"ERROR: Destination path not found:$DESTINATION_PATH">&2;exit1;}[["$CONFLICT"=="inline"||"$CONFLICT"=="rej"]]||{echo"ERROR: --conflict must be inline or rej">&2;exit1;}check_args=(check-update"$DESTINATION_PATH"--quiet)[["$PRERELEASES"=="true"]]&&check_args+=(--prereleases)ifrun_copier"${check_args[@]}";thencheck_code=0;elsecheck_code=$?;fiif[["$check_code"-eq0]];thenecho"No template update available"exit0fiif[["$check_code"-ne2]];thenecho"WARN: check-update returned unexpected code:$check_code">&2diag_args=(check-update"$DESTINATION_PATH"--output-format plain)[["$PRERELEASES"=="true"]]&&diag_args+=(--prereleases)set+e;copier"${diag_args[@]}";set-eexit1fiecho"Template update available"[["$CHECK_ONLY"=="true"]]&&exit2update_args=(update"$DESTINATION_PATH"--defaults--conflict"$CONFLICT")[["$PRERELEASES"=="true"]]&&update_args+=(--prereleases)[["$SKIP_TASKS"=="true"]]&&update_args+=(--skip-tasks)[[-n"$DATA_FILE"]]&&update_args+=(--data-file"$DATA_FILE")forpairin"${DATA_PAIRS[@]}";doupdate_args+=(-d"$pair");doneifrun_copier"${update_args[@]}";thenupdate_code=0;elseupdate_code=$?;fi[["$update_code"-eq0]]||{echo"ERROR: copier update failed with code$update_code">&2;exit1;}answers_file="$DESTINATION_PATH/.copier-answers.yml"[[-f"$answers_file"]]||{echo"ERROR: Answers file missing after update:$answers_file">&2;exit1;}grep-Eq'^_commit:[[:space:]]*[^[:space:]]+'"$answers_file"||{echo"ERROR: Answers file does not contain a valid _commit entry">&2;exit1;}rej_files="$(find"$DESTINATION_PATH"-typef-name'*.rej'-print)"if[[-n"$rej_files"]];thenecho"ERROR: Found .rej files after update. Resolve and remove them before merge.">&2printf'%s\n'"$rej_files"exit1fipushd"$DESTINATION_PATH">/dev/nullset+egitgrep-n"<<<<<<<"--.marker_code=$?set-epopd>/dev/null[["$marker_code"-eq0]]&&{echo"ERROR: Inline merge conflict markers detected after update">&2;exit1;}[["$marker_code"-gt1]]&&echo"WARN: Unable to scan conflict markers with git grep">&2echo"Update completed successfully"git-C"$DESTINATION_PATH"status--short--branch执行示例:
# 首次使用前赋予执行权限chmod+x ./scripts/copier-update-check.sh# 只检查是否有新版本./scripts/copier-update-check.sh --destination-path ./destination --check-only# 自动更新并执行内置检查./scripts/copier-update-check.sh --destination-path ./destination目标是让升级过程“可重复、可追踪、可审计”。
6. 结论与可执行清单
如果你只做三件事,优先级如下:
- 固定升级路径,不混用相对目录。
- 模板发布必须 commit + tag。
- 升级流程固定为
check-update -> update -> 验证。
做到这三点,Copier 基本就能从“能跑”变成“可治理”。
