From 9b6aa8e2fd49e06a62b0c09ce6f66377ff13fc85 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 3 Oct 2026 16:34:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E4=B8=AD=E6=96=87=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E8=A2=AB=20git=20=E8=BD=AC=E4=B9=89=20=E2=86=92=20?= =?UTF-8?q?=E6=8C=89=E9=9C=80=E9=87=8D=E5=BB=BA=E8=AF=AF=E5=88=A4=E4=B8=BA?= =?UTF-8?q?"=E4=B8=A4=E7=AB=AF=E5=85=A8=E5=BB=BA"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首次真机验证按需重建时踩到:`git diff --name-only` 默认 core.quotePath=true, docs/部署说明.md 会输出成 "docs/\351\203\250...",匹配不到 docs/* 规则, 一路掉进"拿不准就两端都重建"的兜底分支——只改文档却重建了 backend + web。 修复: - 新增 classify-changes.sh --git-diff [repo],内部用 `git -c core.quotePath=false diff --name-only`,部署脚本改走它; - 分类时顺带剥掉可能残留的首尾引号; - 补回归用例:临时 git 仓库里提交中文文件名,断言改动列表保留中文原文、 且最终分类为空(不加 --git-diff 时该用例会失败,已实测复现)。 --- .../src/deploy-change-detection.test.ts | 37 +++++++++++++++++++ deploy/lib/classify-changes.sh | 25 +++++++++++-- deploy/ubuntu-deploy.sh | 4 +- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/deploy-change-detection.test.ts b/apps/backend/src/deploy-change-detection.test.ts index d4323126..ddb12285 100644 --- a/apps/backend/src/deploy-change-detection.test.ts +++ b/apps/backend/src/deploy-change-detection.test.ts @@ -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') diff --git a/deploy/lib/classify-changes.sh b/deploy/lib/classify-changes.sh index f98a0d62..e74d5b4c 100644 --- a/deploy/lib/classify-changes.sh +++ b/deploy/lib/classify-changes.sh @@ -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 [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 diff --git a/deploy/ubuntu-deploy.sh b/deploy/ubuntu-deploy.sh index 56a4b7af..a260b25d 100755 --- a/deploy/ubuntu-deploy.sh +++ b/deploy/ubuntu-deploy.sh @@ -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}")"