fix: resolve pnpm in Git Bash pre-push hook

This commit is contained in:
benno0o
2026-08-29 13:40:53 -04:00
committed by haelyra
parent 656d4b5746
commit 03b441792e
2 changed files with 138 additions and 5 deletions
+12 -2
View File
@@ -60,11 +60,21 @@ has_node_script() {
node -e 'const fs=require("fs"); const p=JSON.parse(fs.readFileSync("package.json","utf8")); process.exit(p.scripts && p.scripts[process.argv[1]] ? 0 : 1)' "$script_name" >/dev/null 2>&1
}
run_pnpm() {
if command -v corepack >/dev/null 2>&1; then
corepack pnpm "$@"
elif command -v pnpm >/dev/null 2>&1; then
pnpm "$@"
else
fail "pnpm could not be resolved from PATH or Corepack"
fi
}
run_node_script() {
local pm="$1"
local script_name="$2"
case "$pm" in
pnpm) pnpm run "$script_name" ;;
pnpm) run_pnpm run "$script_name" ;;
bun) bun run "$script_name" ;;
yarn) yarn "$script_name" ;;
npm) npm run "$script_name" ;;
@@ -90,7 +100,7 @@ if [[ -f "package.json" ]]; then
ran_any_check=1
log "Running dependency audit (ECC_PREPUSH_AUDIT=1)"
case "$pm" in
pnpm) pnpm audit --prod || fail "pnpm audit failed" ;;
pnpm) run_pnpm audit --prod || fail "pnpm audit failed" ;;
bun) bun audit || fail "bun audit failed" ;;
yarn) yarn npm audit --recursive || fail "yarn audit failed" ;;
npm) npm audit --omit=dev || fail "npm audit failed" ;;
+126 -3
View File
@@ -11,6 +11,7 @@ const TOML = require('@iarna/toml');
const repoRoot = path.join(__dirname, '..', '..');
const installScript = path.join(repoRoot, 'scripts', 'codex', 'install-global-git-hooks.sh');
const prePushHook = path.join(repoRoot, 'scripts', 'codex-git-hooks', 'pre-push');
const pluginCacheCheckScript = path.join(repoRoot, 'scripts', 'codex', 'check-plugin-cache.js');
const mergeCodexConfigScript = path.join(repoRoot, 'scripts', 'codex', 'merge-codex-config.js');
const mergeMcpConfigScript = path.join(repoRoot, 'scripts', 'codex', 'merge-mcp-config.js');
@@ -42,18 +43,30 @@ function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function runBash(scriptPath, args = [], env = {}, cwd = repoRoot) {
return spawnSync('bash', [scriptPath, ...args], {
function runBash(scriptPath, args = [], env = {}, cwd = repoRoot, input = undefined, preservePath = true) {
const bash = process.platform === 'win32' && fs.existsSync('C:\\Program Files\\Git\\bin\\bash.exe')
? 'C:\\Program Files\\Git\\bin\\bash.exe'
: fs.existsSync('/bin/bash')
? '/bin/bash'
: 'bash';
return spawnSync(bash, [scriptPath, ...args], {
cwd,
env: {
...process.env,
...(preservePath ? process.env : {}),
...env,
},
encoding: 'utf8',
input,
stdio: ['pipe', 'pipe', 'pipe'],
});
}
function toBashPath(filePath) {
return process.platform === 'win32'
? `/${filePath[0].toLowerCase()}${filePath.slice(2).replaceAll('\\', '/')}`
: filePath;
}
function runNode(scriptPath, args = [], env = {}, cwd = repoRoot) {
return spawnSync('node', [scriptPath, ...args], {
cwd,
@@ -116,6 +129,116 @@ const cacheManifestWithLocalRefs = {
let passed = 0;
let failed = 0;
function makeExecutable(filePath, content) {
fs.writeFileSync(filePath, content, { mode: 0o755 });
fs.chmodSync(filePath, 0o755);
}
function runHermeticPrePush({ failScript = null, includeCorepack = true, includePnpm = false } = {}) {
const tempDir = createTempDir('codex-pre-push-');
const binDir = path.join(tempDir, 'bin');
const projectDir = path.join(tempDir, 'project');
const callsPath = path.join(tempDir, 'calls.txt');
const bashEnv = path.join(tempDir, 'bash-env');
fs.mkdirSync(binDir);
fs.mkdirSync(projectDir);
const functionStub = (name, corepack) => `${name}() {
printf '%s\\n' "${corepack ? '' : 'pnpm '}$*" >> "${toBashPath(callsPath)}"
${corepack ? 'shift' : ':'}
shift
test "$1" != "${failScript || '__never__'}"
}`;
fs.writeFileSync(
bashEnv,
`git() { return 0; }
node() { "${toBashPath(process.execPath)}" "$@"; }
${includeCorepack ? functionStub('corepack', true) : ''}
${includePnpm ? functionStub('pnpm', false) : ''}
`,
);
fs.writeFileSync(path.join(projectDir, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n');
const initialized = spawnSync('git', ['init', '--quiet'], { cwd: projectDir });
assert.strictEqual(initialized.status, 0, initialized.stderr?.toString());
writeJson(path.join(projectDir, 'package.json'), {
packageManager: 'pnpm@11.9.0',
scripts: { lint: 'x', typecheck: 'x', test: 'x', build: 'x' },
});
const result = runBash(
prePushHook,
[],
{
PATH: toBashPath(binDir),
BASH_ENV: toBashPath(bashEnv),
ECC_PREPUSH_AUDIT: '0',
ECC_SKIP_GIT_HOOKS: '0',
ECC_SKIP_PREPUSH: '0',
MSYS_NO_PATHCONV: '1',
},
projectDir,
Buffer.from('refs/heads/main 1111111111111111111111111111111111111111 refs/heads/main 0000000000000000000000000000000000000000\n'),
false,
);
const calls = fs.existsSync(callsPath)
? fs.readFileSync(callsPath, 'utf8').trim().split(/\r?\n/)
: [];
cleanup(tempDir);
return { result, calls };
}
if (
test('pre-push uses Corepack pinned pnpm and runs every required verification script', () => {
const { result, calls } = runHermeticPrePush();
assert.strictEqual(result.status, 0, JSON.stringify(result, null, 2));
assert.deepStrictEqual(calls, [
'pnpm run lint',
'pnpm run typecheck',
'pnpm run test',
'pnpm run build',
], JSON.stringify(result, null, 2));
})
)
passed++;
else failed++;
if (
test('pre-push falls back to direct pnpm when Corepack is absent', () => {
const { result, calls } = runHermeticPrePush({
includeCorepack: false,
includePnpm: true,
});
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.deepStrictEqual(calls, [
'pnpm run lint',
'pnpm run typecheck',
'pnpm run test',
'pnpm run build',
]);
})
)
passed++;
else failed++;
if (
test('pre-push fails closed when pnpm and Corepack cannot resolve', () => {
const { result } = runHermeticPrePush({ includeCorepack: false });
assert.notStrictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stderr, /pnpm.*(?:resolve|found)/i);
})
)
passed++;
else failed++;
if (
test('pre-push stops immediately when a required verification script fails', () => {
const { result, calls } = runHermeticPrePush({ failScript: 'typecheck' });
assert.notStrictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.deepStrictEqual(calls, ['pnpm run lint', 'pnpm run typecheck']);
assert.match(result.stderr, /typecheck failed/);
})
)
passed++;
else failed++;
if (
test('check-plugin-cache fails when the installed cache is missing manifest-referenced files', () => {
const homeDir = createTempDir('codex-plugin-cache-home-');