如果你已经能跑 copier copy,但一到 check-update、update 就反复踩坑,这通常不是工具本身不稳定,而是缺少一套可复用的工程闭环。本文把最核心的 5 个问题合并成一篇:最小闭环怎么跑、升级为什么失败、报错怎么排查、为什么要打 tag、如何接入团队 CI。
1. 先把最小闭环跑通
Copier 在团队里要稳定,最低闭环是:
text
copy -> 模板发布(tag) -> check-update -> update -> 验证提交
基础检查:
bash
copier -v
git --version
最小命令:
bash
copier copy ./my_copier_template ./destination -d project_name=demo
copier check-update ./destination
copier update ./destination --defaults
成功判定:
bash
cd ./destination
git status
2. 为什么升级经常失败
高频不是"命令拼错",而是以下四类问题:
- 路径混用:
./destination和../destination在不同 cwd 下可能是两个目录。 - 引用丢失:
.copier-answers.yml缺少可追踪模板版本信息。 - 版本漂移:模板仓库没打 tag,更新来源不稳定。
- 变量断档:新增必填问题没有 default,也没在命令里
-d传值。
建议固定排查顺序:
text
路径 -> 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 的价值:
- 可追踪:明确目标项目基于哪个模板版本。
- 可回滚:出问题时能回到稳定版本。
- 可协作:多人对版本语义有共识。
推荐发布动作:
bash
git add .
git commit -m "template: release v0.0.5"
git tag v0.0.5
git push --follow-tags
5. 团队落地:培训 + CI 自动化并行
个人可用不等于团队可用。建议双线并行:
- 培训线:统一路径、统一版本规则、统一排障顺序。
- 自动化线:定时
check-update,有更新再update,有冲突就阻断。
脚本入口可以统一放在:
scripts/copier-update-check.sh
完整脚本如下(可直接复制保存为 scripts/copier-update-check.sh):
bash
#!/usr/bin/env bash
set -euo pipefail
DESTINATION_PATH="./destination"
CONFLICT="inline"
PRERELEASES=false
SKIP_TASKS=false
CHECK_ONLY=false
DATA_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() {
local name="$1"
if ! command -v "$name" >/dev/null 2>&1; then
echo "ERROR: Required command not found: $name" >&2
exit 1
fi
}
run_copier() {
set +e
copier "$@"
local code=$?
set -e
return "$code"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--destination-path) DESTINATION_PATH="$2"; shift 2 ;;
-d|--data) DATA_PAIRS+=("$2"); shift 2 ;;
--data-file) DATA_FILE="$2"; shift 2 ;;
--conflict) CONFLICT="$2"; shift 2 ;;
--prereleases) PRERELEASES=true; shift ;;
--skip-tasks) SKIP_TASKS=true; shift ;;
--check-only) CHECK_ONLY=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
done
require_command copier
require_command git
[[ -d "$DESTINATION_PATH" ]] || { echo "ERROR: Destination path not found: $DESTINATION_PATH" >&2; exit 1; }
[[ "$CONFLICT" == "inline" || "$CONFLICT" == "rej" ]] || { echo "ERROR: --conflict must be inline or rej" >&2; exit 1; }
check_args=(check-update "$DESTINATION_PATH" --quiet)
[[ "$PRERELEASES" == "true" ]] && check_args+=(--prereleases)
if run_copier "${check_args[@]}"; then check_code=0; else check_code=$?; fi
if [[ "$check_code" -eq 0 ]]; then
echo "No template update available"
exit 0
fi
if [[ "$check_code" -ne 2 ]]; then
echo "WARN: check-update returned unexpected code: $check_code" >&2
diag_args=(check-update "$DESTINATION_PATH" --output-format plain)
[[ "$PRERELEASES" == "true" ]] && diag_args+=(--prereleases)
set +e; copier "${diag_args[@]}"; set -e
exit 1
fi
echo "Template update available"
[[ "$CHECK_ONLY" == "true" ]] && exit 2
update_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")
for pair in "${DATA_PAIRS[@]}"; do update_args+=(-d "$pair"); done
if run_copier "${update_args[@]}"; then update_code=0; else update_code=$?; fi
[[ "$update_code" -eq 0 ]] || { echo "ERROR: copier update failed with code $update_code" >&2; exit 1; }
answers_file="$DESTINATION_PATH/.copier-answers.yml"
[[ -f "$answers_file" ]] || { echo "ERROR: Answers file missing after update: $answers_file" >&2; exit 1; }
grep -Eq '^_commit:[[:space:]]*[^[:space:]]+' "$answers_file" || { echo "ERROR: Answers file does not contain a valid _commit entry" >&2; exit 1; }
rej_files="$(find "$DESTINATION_PATH" -type f -name '*.rej' -print)"
if [[ -n "$rej_files" ]]; then
echo "ERROR: Found .rej files after update. Resolve and remove them before merge." >&2
printf '%s\n' "$rej_files"
exit 1
fi
pushd "$DESTINATION_PATH" >/dev/null
set +e
git grep -n "<<<<<<<" -- .
marker_code=$?
set -e
popd >/dev/null
[[ "$marker_code" -eq 0 ]] && { echo "ERROR: Inline merge conflict markers detected after update" >&2; exit 1; }
[[ "$marker_code" -gt 1 ]] && echo "WARN: Unable to scan conflict markers with git grep" >&2
echo "Update completed successfully"
git -C "$DESTINATION_PATH" status --short --branch
执行示例:
bash
# 首次使用前赋予执行权限
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 基本就能从"能跑"变成"可治理"。