修复 Hook 链继承仓库定位变量导致真实仓库被夹具污染
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m47s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m53s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m48s
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / Frontend tests (push) Failing after 4m6s
Project CI / Repository checks (push) Successful in 3m57s
Project CI / Native shell tests (push) Successful in 7m40s
Project CI / Backend tests (push) Successful in 8m27s
Project CI / AI game creator shell web tests (push) Successful in 12m53s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m47s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m53s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m48s
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / Frontend tests (push) Failing after 4m6s
Project CI / Repository checks (push) Successful in 3m57s
Project CI / Native shell tests (push) Successful in 7m40s
Project CI / Backend tests (push) Successful in 8m27s
Project CI / AI game creator shell web tests (push) Successful in 12m53s
- .husky/pre-commit 与 .husky/pre-push 入口清除 GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE 等仓库定位变量 - scripts/check-repository-ci.sh 增加同样的清理,保证本地门禁与 CI 走同一套隔离 - scripts/git-hooks.test.mjs 夹具 Git 调用前自检仓库归属并禁用 Hook,命令落到外部仓库时直接失败 - 守卫用例的子进程必须真的继承 GIT_DIR,避免隔离断言空转 - 更新开发运维文档与 shared-memory/pitfalls.md,记录链接工作树注入形态与配置修复步骤
This commit is contained in:
@@ -1 +1,4 @@
|
||||
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
||||
# 子进程(npm、lint-staged、测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
||||
npm run format:staged
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
||||
# 钩子链(npm → check:repository-ci → 测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
||||
npm run check:pre-push-master -- "$@"
|
||||
|
||||
@@ -5614,5 +5614,8 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
|
||||
- 在 hook 内运行临时仓库测试时,`cwd` 不会覆盖继承的 `GIT_DIR`、`GIT_WORK_TREE` 或 `GIT_INDEX_FILE`。未隔离的 Git/lint-staged 子进程可能向真实仓库提交 fixture,甚至把测试版 ESLint、Prettier 配置带入主分支。
|
||||
- fixture 子进程统一清除 `GIT_*` 环境,并用一次性外层 linked worktree 验证引用、索引、配置不变;原有工程检查规则保持完整,不能用逐项关闭规则修复 fixture 污染。
|
||||
- 2026-09-16 复核:从链接工作树 `git push`/`git commit` 时,Git 注入 `GIT_DIR=<主仓库>/.git/worktrees/<name>`、`GIT_WORK_TREE`、`GIT_INDEX_FILE`,husky → npm → `check:repository-ci` → 夹具测试整链条继承。夹具 `git config user.name "Git Hooks Test"` 会写进共享 `.git/config`(此后所有提交 author 变成 `Git Hooks Test`);夹具 `git init` 按是否带 `GIT_WORK_TREE` 分别写成 `core.bare=true`(`fatal: this operation must be run in a work tree`)或 `core.worktree=<临时夹具目录>`(`git status` 实际在操作临时目录)。
|
||||
- 处理:钩子与门禁入口先 `unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES`;夹具 Git 调用在命令前自检 `rev-parse --show-toplevel` 等于夹具目录,落到外部仓库立即失败;守卫用例的子进程必须真的继承 `GIT_DIR`,否则断言会空转。
|
||||
- 验证:`git config --show-origin --get user.name` 出现 `file:.git/config Git Hooks Test`、`git rev-parse --show-toplevel` 指向 `%TEMP%\genarrative-pre-push-*\repo` 都是被污染的确定性证据;被 `core.worktree` 劫持期间执行的 `git pull` 会把检出写进临时目录,真实工作树整体落后(本次 93 个文件),配置修好后用 `git checkout HEAD -- .` 回填。
|
||||
- Vitest 的 `toHaveBeenCalledWith` 匹配任意一次调用,失败输出会列出其它命令;应先定位相同命令的真实参数差异,不能由其它调用的序号推断时序故障。
|
||||
- 存在后台轮询的 IPC mock 不应要求目标命令占据全局最后一次调用。验证刷新时先记录调用边界,再筛选该边界之后的目标命令,严格核对其最后一次参数,避免后台查询影响断言,也避免旧调用掩盖刷新未执行。
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
Git hook 的临时仓库测试必须清除子进程继承的仓库定位环境(例如 `GIT_DIR`、`GIT_WORK_TREE`、`GIT_INDEX_FILE`);仅设置 `cwd` 不能隔离 Git。回归应从带这些变量的外层仓库运行,验证外层引用、索引与配置不变。临时测试文件必须留在独立目录并清理,不得通过测试生成主仓库提交或覆盖 ESLint、Prettier 配置。修复 lint 配置时保留原有规则、忽略范围与零警告门禁,不以关闭规则代替排障。
|
||||
|
||||
链接工作树(`git worktree`)里执行 `git push`、`git commit` 时,Git 会把 `GIT_DIR`(指向该工作树的管理目录)、`GIT_WORK_TREE`、`GIT_INDEX_FILE` 注入 Hook,`npm`、`lint-staged` 与测试夹具整链条继承;夹具的 `git config` 会写到共享 `.git/config`(此后所有提交 author 变成夹具身份),夹具的 `git init` 会重写 `core.bare` 或 `core.worktree`。因此 `.husky/pre-commit`、`.husky/pre-push` 与 `scripts/check-repository-ci.sh` 入口必须 `unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES`,夹具 Git 调用还要自检 `git rev-parse --show-toplevel` 等于夹具目录,落到外部仓库时直接失败。真实仓库被污染后的修复顺序:备份 `.git/config` → `git config --unset core.worktree` → 确认 `core.bare=false` → `git config --unset user.name` 与 `user.email` 恢复全局身份 → 核对 `.git/worktrees/*/config.worktree` → 用 `git checkout HEAD -- .` 回填被劫持期间漏掉的检出(未跟踪文件不动)。
|
||||
|
||||
Stdb 发布以 root 准备文件、再切换 `spacetimedb` 用户执行时,WASM 必须放在服务用户可遍历的父目录下。生产脚本在 `/var/tmp` 创建随机私有目录并移交给发布用户,退出时清理;不能仅修改子目录所有权后继续从 `/root` 下读取。发布成功的判据仍是完整 Full Build 的 Stdb、API、Web 发布及退出维护模式全部成功。
|
||||
|
||||
更新时间:`2026-08-05`
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 链接工作树推送时 Git 会向 Hook 注入 GIT_DIR 等仓库定位变量;npm 链上的测试夹具会继承
|
||||
# 它们并绕过 cwd 写到真实仓库,这里统一清除,保证门禁只作用于当前工作树。
|
||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
||||
|
||||
base_ref="${SPACETIME_SCHEMA_BASE_REF:-${1:-}}"
|
||||
head_ref="${REPOSITORY_CI_HEAD_REF:-${2:-HEAD}}"
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
@@ -20,6 +21,8 @@ const packageJson = JSON.parse(
|
||||
);
|
||||
|
||||
test('pre-commit hook fixes staged imports and formatting without swallowing unstaged work', () => {
|
||||
assertGuardChildInheritsPoisonedEnvironment();
|
||||
|
||||
assert.equal(packageJson.scripts.prepare, 'husky');
|
||||
assert.equal(packageJson.scripts['format:staged'], 'lint-staged');
|
||||
assert.deepEqual(packageJson['lint-staged'], {
|
||||
@@ -29,9 +32,10 @@ test('pre-commit hook fixes staged imports and formatting without swallowing uns
|
||||
],
|
||||
'*.rs': ['node scripts/lint-staged-rustfmt.mjs'],
|
||||
});
|
||||
assert.equal(
|
||||
assertHookClearsGitEnvironment(
|
||||
'pre-commit',
|
||||
readFileSync(join(repoRoot, '.husky', 'pre-commit'), 'utf8'),
|
||||
'npm run format:staged\n',
|
||||
'npm run format:staged',
|
||||
);
|
||||
|
||||
const tempRepo = createTempDirectory('genarrative-git-hooks-');
|
||||
@@ -162,6 +166,8 @@ test('pre-commit hook fixes staged imports and formatting without swallowing uns
|
||||
});
|
||||
|
||||
test('pre-push runs repository parity only for master updates', () => {
|
||||
assertGuardChildInheritsPoisonedEnvironment();
|
||||
|
||||
const tempDir = createTempDirectory('genarrative-pre-push-');
|
||||
try {
|
||||
const npmLog = join(tempDir, 'repo', 'npm.log');
|
||||
@@ -291,10 +297,12 @@ test('hook fixtures do not mutate the calling linked worktree or its index', ()
|
||||
env: {
|
||||
...isolatedGitEnvironment(),
|
||||
NODE_TEST_CONTEXT: undefined,
|
||||
GENARRATIVE_HOOK_GUARD_CHILD: '1',
|
||||
GIT_DIR: gitDir,
|
||||
GIT_COMMON_DIR: join(outerRepo, '.git'),
|
||||
GIT_WORK_TREE: worktree,
|
||||
GIT_INDEX_FILE: indexPath,
|
||||
GIT_PREFIX: '',
|
||||
GIT_CONFIG_COUNT: '1',
|
||||
GIT_CONFIG_KEY_0: 'core.worktree',
|
||||
GIT_CONFIG_VALUE_0: worktree,
|
||||
@@ -353,7 +361,15 @@ test('Gitea Repository checks and master pre-push share the same repository comm
|
||||
);
|
||||
assert.match(repositoryScript, /npm run build/u);
|
||||
assert.match(repositoryScript, /git diff --check/u);
|
||||
assert.equal(prePushHook, 'npm run check:pre-push-master -- "$@"\n');
|
||||
assertHookClearsGitEnvironment(
|
||||
'pre-push',
|
||||
prePushHook,
|
||||
'npm run check:pre-push-master -- "$@"',
|
||||
);
|
||||
assert.match(
|
||||
repositoryScript,
|
||||
/^unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR/mu,
|
||||
);
|
||||
});
|
||||
|
||||
function readFileOrEmpty(path) {
|
||||
@@ -376,11 +392,38 @@ function toBashPath(path) {
|
||||
}
|
||||
|
||||
function git(cwd, ...args) {
|
||||
return execFileSync('git', args, {
|
||||
const env = isolatedGitEnvironment();
|
||||
assertFixtureRepository(cwd, env);
|
||||
return execFileSync(
|
||||
'git',
|
||||
['--no-pager', '-c', `core.hooksPath=${nullDevice}`, ...args],
|
||||
{
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 夹具命令必须落在夹具自己的仓库:继承的 GIT_DIR/GIT_WORK_TREE 优先级高于 cwd,曾让夹具把
|
||||
// 身份、core.bare 与 core.worktree 写进调用方的真实仓库。
|
||||
function assertFixtureRepository(cwd, env) {
|
||||
const probe = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
env: isolatedGitEnvironment(),
|
||||
env,
|
||||
});
|
||||
if (probe.status !== 0) {
|
||||
// 此时夹具目录还不是仓库(例如首次 git init),命令本身会把它建起来。
|
||||
return;
|
||||
}
|
||||
const toplevel = realpathSync.native(probe.stdout.trim());
|
||||
const expected = realpathSync.native(resolve(cwd));
|
||||
assert.equal(
|
||||
toplevel,
|
||||
expected,
|
||||
`夹具 Git 命令会落到外部仓库:cwd=${expected} toplevel=${toplevel}`,
|
||||
);
|
||||
}
|
||||
|
||||
function isolatedGitEnvironment() {
|
||||
@@ -391,6 +434,41 @@ function isolatedGitEnvironment() {
|
||||
);
|
||||
}
|
||||
|
||||
// 守卫用例把本文件跑在毒化环境里;子进程必须真的继承 GIT_DIR,否则断言会空转。
|
||||
function assertGuardChildInheritsPoisonedEnvironment() {
|
||||
if (process.env.GENARRATIVE_HOOK_GUARD_CHILD !== '1') {
|
||||
return;
|
||||
}
|
||||
assert.ok(process.env.GIT_DIR, '守卫子进程必须继承 GIT_DIR');
|
||||
assert.equal(
|
||||
Object.hasOwn(isolatedGitEnvironment(), 'GIT_DIR'),
|
||||
false,
|
||||
'夹具子进程环境必须清除 GIT_DIR',
|
||||
);
|
||||
}
|
||||
|
||||
function assertHookClearsGitEnvironment(hookName, contents, command) {
|
||||
const lines = contents.trimEnd().split('\n');
|
||||
assert.equal(
|
||||
lines.at(-1),
|
||||
command,
|
||||
`${hookName} 最后一行必须保持原有钩子命令`,
|
||||
);
|
||||
const unsetLine = lines.find((line) => line.startsWith('unset '));
|
||||
assert.ok(unsetLine, `${hookName} 必须先清除 Git 注入的仓库定位变量`);
|
||||
const cleared = new Set(unsetLine.split(/\s+/u));
|
||||
for (const variable of [
|
||||
'GIT_DIR',
|
||||
'GIT_WORK_TREE',
|
||||
'GIT_INDEX_FILE',
|
||||
'GIT_COMMON_DIR',
|
||||
]) {
|
||||
assert.ok(cleared.has(variable), `${hookName} 必须清除 ${variable}`);
|
||||
}
|
||||
}
|
||||
|
||||
const nullDevice = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
||||
|
||||
function createTempDirectory(prefix) {
|
||||
const tempRoot = join(homedir(), 'data', 'tmp');
|
||||
mkdirSync(tempRoot, { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user