Skip to content

Commit 044327b

Browse files
fix: make husky prepare resilient in docker installs
1 parent d8f62b4 commit 044327b

4 files changed

Lines changed: 92 additions & 1 deletion

File tree

.changeset/chatty-steaks-fall.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
fix: prevent crash when husky is not available during setup

.husky/install.mjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { existsSync } from 'node:fs';
2+
3+
// Skip Husky installation in environments where hooks are not needed
4+
if (
5+
process.env.NODE_ENV === 'production' ||
6+
process.env.CI === 'true' ||
7+
process.env.HUSKY === '0' ||
8+
!existsSync('.git')
9+
) {
10+
process.exit(0);
11+
}
12+
13+
try {
14+
const { install } = await import('husky');
15+
16+
// Only call install if it exists (safe check)
17+
if (typeof install === 'function') {
18+
install();
19+
}
20+
} catch (error) {
21+
// Ignore error if husky is not installed (e.g., production/CI environments)
22+
if (
23+
error?.code === 'ERR_MODULE_NOT_FOUND' ||
24+
error?.message?.includes("Cannot find package 'husky'")
25+
) {
26+
process.exit(0);
27+
}
28+
29+
// Re-throw other unexpected errors
30+
throw error;
31+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
"docker:cover:integration": "pnpm run docker:integration:run pnpm exec nyc --report-dir .coverage/integration pnpm run test:integration -- -p cover",
9696
"postdocker:integration:run": "docker compose -f ./test/integration/docker-compose.yml down",
9797
"prepack": "pnpm run build",
98-
"prepare": "husky install || exit 0",
98+
"prepare": "node -e \"if(require('fs').existsSync('.husky/install.mjs'))require('child_process').execSync('node .husky/install.mjs',{stdio:'inherit'})\"",
9999
"changeset:version": "changeset version && pnpm install --lockfile-only",
100100
"changeset:publish": "changeset publish"
101101
},

test/unit/husky/install.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { expect } from 'chai'
2+
import fs from 'fs'
3+
import os from 'os'
4+
import path from 'path'
5+
import { spawnSync } from 'child_process'
6+
7+
const projectRoot = process.cwd()
8+
const installScriptPath = path.join(projectRoot, '.husky', 'install.mjs')
9+
10+
const runInstall = (cwd: string, env: NodeJS.ProcessEnv = {}) => {
11+
return spawnSync('node', [installScriptPath], {
12+
cwd,
13+
env: {
14+
...process.env,
15+
...env,
16+
},
17+
encoding: 'utf-8',
18+
})
19+
}
20+
21+
describe('husky install script', () => {
22+
it('exits successfully when .git is missing', () => {
23+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nostream-husky-no-git-'))
24+
25+
try {
26+
const result = runInstall(tmpDir)
27+
expect(result.status).to.equal(0)
28+
} finally {
29+
fs.rmSync(tmpDir, { recursive: true, force: true })
30+
}
31+
})
32+
33+
it('exits successfully when HUSKY is disabled', () => {
34+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nostream-husky-disabled-'))
35+
36+
try {
37+
const result = runInstall(tmpDir, { HUSKY: '0' })
38+
expect(result.status).to.equal(0)
39+
} finally {
40+
fs.rmSync(tmpDir, { recursive: true, force: true })
41+
}
42+
})
43+
44+
it('exits successfully when husky package is unavailable even if .git exists', () => {
45+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nostream-husky-missing-package-'))
46+
47+
try {
48+
fs.mkdirSync(path.join(tmpDir, '.git'))
49+
const result = runInstall(tmpDir)
50+
expect(result.status).to.equal(0)
51+
} finally {
52+
fs.rmSync(tmpDir, { recursive: true, force: true })
53+
}
54+
})
55+
})

0 commit comments

Comments
 (0)