fix(deploy): 中文路径被 git 转义 → 按需重建误判为"两端全建"

首次真机验证按需重建时踩到:`git diff --name-only` 默认 core.quotePath=true,
docs/部署说明.md 会输出成 "docs/\351\203\250...",匹配不到 docs/* 规则,
一路掉进"拿不准就两端都重建"的兜底分支——只改文档却重建了 backend + web。

修复:
- 新增 classify-changes.sh --git-diff <from> <to> [repo],内部用
  `git -c core.quotePath=false diff --name-only`,部署脚本改走它;
- 分类时顺带剥掉可能残留的首尾引号;
- 补回归用例:临时 git 仓库里提交中文文件名,断言改动列表保留中文原文、
  且最终分类为空(不加 --git-diff 时该用例会失败,已实测复现)。
This commit is contained in:
yml2213
2026-10-03 16:34:18 +08:00
parent 0b48313617
commit 9b6aa8e2fd
3 changed files with 62 additions and 4 deletions
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
@@ -21,6 +22,21 @@ function classify(files: string[]): string {
}).trim()
}
function withTempGitRepo(fn: (repo: string, git: (...args: string[]) => string) => void): void {
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'deploy-classify-'))
const git = (...args: string[]) =>
execFileSync('git', ['-C', repo, '-c', 'user.email=t@t', '-c', 'user.name=t', ...args], {
encoding: 'utf8',
})
try {
git('init', '-q')
git('commit', '-q', '--allow-empty', '-m', 'base')
fn(repo, git)
} finally {
fs.rmSync(repo, { recursive: true, force: true })
}
}
test(
'改动分类:只改后端源码只重建 backend,只改前端源码只重建 web',
{ skip: !hasDeployScripts },
@@ -69,11 +85,32 @@ test('部署脚本:按改动重建,不再无脑两端全量构建', { skip:
assert.ok(!source.includes('log "构建 Web 镜像"'), '旧的写死 Web 构建仍在')
})
test('改动分类:中文文件名不会被 git 转义成"漏判 → 两端全建"', { skip: !hasDeployScripts }, () => {
// 真实踩过的坑:docs/部署说明.md 被 git 输出成 "docs/\351\203\250...",
// 匹配不到 docs/* 规则,直接掉进兜底分支把 backend + web 都重建了。
withTempGitRepo((repo, git) => {
fs.mkdirSync(path.join(repo, 'docs'), { recursive: true })
fs.writeFileSync(path.join(repo, 'docs', '部署说明.md'), 'v1')
git('add', '-A')
git('commit', '-q', '-m', 'docs: 中文文件名')
const changed = execFileSync('bash', [CLASSIFIER, '--git-diff', 'HEAD~1', 'HEAD', repo], {
encoding: 'utf8',
})
assert.ok(
changed.includes('docs/部署说明.md'),
`git 改动列表应保留中文原文,实际输出:${changed.trim()}`,
)
assert.equal(classify(changed.split('\n').filter(Boolean)), '')
})
})
test('部署脚本:基础镜像只补缺、部署状态在健康检查之后才落盘', { skip: !hasDeployScripts }, () => {
const source = fs.readFileSync(DEPLOY_SCRIPT, 'utf8')
assert.ok(source.includes('--policy missing'), 'compose pull 应使用 --policy missing')
assert.ok(source.includes('write_deploy_state'), '缺少部署状态落盘')
assert.ok(source.includes('print_timing_summary'), '缺少阶段耗时输出')
assert.ok(source.includes('--git-diff'), '改动列表应通过 classify-changes.sh --git-diff 获取')
const healthIndex = source.indexOf('wait_for_health\n')
const stateIndex = source.indexOf('write_deploy_state\n')
+22 -3
View File
@@ -3,7 +3,7 @@
#
# 用法:
# printf '%s\n' "apps/backend/src/x.ts" | bash deploy/lib/classify-changes.sh
# bash deploy/lib/classify-changes.sh --paths a.txt b.txt
# bash deploy/lib/classify-changes.sh --git-diff <from> <to> [repo] # 打印改动的文件列表
#
# 输出:空格分隔的服务名(backend / web),两端都不用重建时输出空行。
#
@@ -13,15 +13,26 @@
# backend 镜像 = apps/backend/**(排除 .dockerignore 里的 node_modules/dist/data)
# web 镜像 = apps/frontend/** + deploy/caddy/Caddyfile
# 取改动文件列表。必须关掉 core.quotePath:
# 否则中文路径会输出成 "docs/\351\203\250..." 这种 C 转义带引号的形式,
# 匹配不到 docs/* 之类的规则、一路掉进"两端都重建"的兜底分支
# (2026-10-03 真实踩过一次:只改文档却重建了 backend + web)。
print_git_diff() {
local from="$1" to="$2" repo="${3:-.}"
git -C "${repo}" -c core.quotePath=false diff --name-only "${from}" "${to}"
}
classify_changed_paths() {
local backend=0 web=0 path
local -a out=()
# `|| [[ -n "${path}" ]]` 兜住"最后一行没有换行符"的情况(read 在 EOF 会返回非零)
while IFS= read -r path || [[ -n "${path}" ]]; do
# 去掉 git 输出里可能带的空白
# 去掉 git 输出里可能带的空白与残留引号
path="${path#"${path%%[![:space:]]*}"}"
path="${path%"${path##*[![:space:]]}"}"
path="${path#\"}"
path="${path%\"}"
[[ -n "${path}" ]] || continue
case "${path}" in
@@ -54,4 +65,12 @@ classify_changed_paths() {
printf '%s\n' "${out[*]:-}"
}
classify_changed_paths
case "${1:-}" in
--git-diff)
shift
print_git_diff "$@"
;;
*)
classify_changed_paths
;;
esac
+3 -1
View File
@@ -267,7 +267,9 @@ detect_changed_services() {
return
fi
CHANGED_FILES="$(git -C "${PROJECT_ROOT}" diff --name-only "${last_sha}" "${head_sha}" 2>/dev/null || true)"
# 走 classify-changes.sh 的 --git-diff 而不是直接 git diff:
# 那里关掉了 core.quotePath,中文路径(docs/xxx.md)不会被转义成 "\351..." 而匹配不上规则。
CHANGED_FILES="$(bash "${PROJECT_ROOT}/deploy/lib/classify-changes.sh" --git-diff "${last_sha}" "${head_sha}" 2>/dev/null || true)"
DIFF_KNOWN=true
env_hash="$(file_sha256 "${ENV_FILE}")"