From 03b441792e61914c7a6a792ebe97fe8b9acaa5d2 Mon Sep 17 00:00:00 2001 From: benno0o Date: Sun, 26 Jul 2026 23:08:48 +0200 Subject: [PATCH] fix: resolve pnpm in Git Bash pre-push hook --- scripts/codex-git-hooks/pre-push | 14 +++- tests/scripts/codex-hooks.test.js | 129 +++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/scripts/codex-git-hooks/pre-push b/scripts/codex-git-hooks/pre-push index 82a6b0261..3d388e8c3 100644 --- a/scripts/codex-git-hooks/pre-push +++ b/scripts/codex-git-hooks/pre-push @@ -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" ;; diff --git a/tests/scripts/codex-hooks.test.js b/tests/scripts/codex-hooks.test.js index 1c49f4c63..a61c5bfea 100644 --- a/tests/scripts/codex-hooks.test.js +++ b/tests/scripts/codex-hooks.test.js @@ -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-');