secure-husky-setup 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/ci.js +103 -104
  2. package/lib/hooks.js +10 -9
  3. package/package.json +1 -1
package/lib/ci.js CHANGED
@@ -8,7 +8,6 @@ const { logInfo, logSuccess, logError } = require('./logger');
8
8
  const TEMPLATE_PATH = path.resolve(__dirname, '../templates/ci-tests.yml');
9
9
 
10
10
  exports.setupCIScript = async (gitRoot) => {
11
- const projectDir = path.relative(gitRoot, process.cwd()) || '.';
12
11
  const scriptsDir = path.join(process.cwd(), 'scripts');
13
12
  const scriptPath = path.join(scriptsDir, 'run-ci-checks.sh');
14
13
 
@@ -20,7 +19,7 @@ exports.setupCIScript = async (gitRoot) => {
20
19
  logInfo("Creating scripts/run-ci-checks.sh...");
21
20
  }
22
21
 
23
- await fs.writeFile(scriptPath, buildCIScript(projectDir));
22
+ await fs.writeFile(scriptPath, buildCIScript());
24
23
  await fs.chmod(scriptPath, 0o755);
25
24
  logSuccess("scripts/run-ci-checks.sh created.");
26
25
  logInfo("To move tests to pre-commit in future: add './scripts/run-ci-checks.sh' to .husky/pre-commit.");
@@ -35,6 +34,7 @@ exports.setupPrePushHook = async (gitRoot) => {
35
34
  return;
36
35
  }
37
36
 
37
+ // relative path from gitRoot to project e.g. "server" or "."
38
38
  const projectDir = path.relative(gitRoot, process.cwd()) || '.';
39
39
 
40
40
  if (await fs.pathExists(hookPath)) {
@@ -114,26 +114,31 @@ exports.ensurePackageLock = async () => {
114
114
  };
115
115
 
116
116
  function buildPrePushHook(projectDir) {
117
+ // pre-push hook runs from git root
118
+ // if project is in a subfolder, cd into it first so npm start/test work correctly
119
+ // but git commands (rev-parse) in run-ci-checks.sh work from git root
117
120
  const cdLine = projectDir !== '.' ? `cd "${projectDir}"` : '';
118
121
  return `#!/bin/sh
119
122
 
120
- # Pre-push hook — Newman + Smoke Tests
121
- ${cdLine ? cdLine + '\n' : ''}
122
- ./scripts/run-ci-checks.sh
123
+ # Pre-push hook — runs from git root
124
+ # cd into project subfolder so npm commands work
125
+ ${cdLine ? cdLine + '\n' : ''}./scripts/run-ci-checks.sh
123
126
  `;
124
127
  }
125
128
 
126
- function buildCIScript(projectDir) {
129
+ function buildCIScript() {
130
+ // This script runs from project directory (after cd in pre-push hook)
131
+ // git commands use GIT_DIR env or walk up to find .git automatically
127
132
  return `#!/bin/sh
128
133
 
129
134
  # ---------------------------------------------------------------
130
- # run-ci-checks.sh — Smoke Tests + Newman API Tests
135
+ # run-ci-checks.sh
136
+ # Runs from project directory. Git commands work because git
137
+ # automatically finds .git by walking up the directory tree.
131
138
  # ---------------------------------------------------------------
132
139
 
133
- # ---------------------------------------------------------------
134
140
  # Git diff check — only run if actual files changed
135
- # ---------------------------------------------------------------
136
- LOCAL=$(git rev-parse @)
141
+ LOCAL=$(git rev-parse @ 2>/dev/null)
137
142
  REMOTE=$(git rev-parse @{u} 2>/dev/null)
138
143
 
139
144
  if [ "$REMOTE" != "" ] && [ "$LOCAL" = "$REMOTE" ]; then
@@ -155,130 +160,124 @@ fi
155
160
  echo ""
156
161
  echo "[CI Checks] Changed files detected:"
157
162
  echo "$CHANGED" | sed 's/^/ -> /'
158
-
159
163
  echo ""
160
164
  echo "[CI Checks] Starting checks..."
161
165
 
162
- # ---------------------------------------------------------------
163
166
  # Check if start and test scripts exist
164
- # ---------------------------------------------------------------
165
167
  START_SCRIPT=$(node -e "try{const p=require('./package.json');console.log(p.scripts&&p.scripts.start?'yes':'no')}catch(e){console.log('no')}" 2>/dev/null)
166
168
  TEST_SCRIPT=$(node -e "try{const p=require('./package.json');console.log(p.scripts&&p.scripts.test?'yes':'no')}catch(e){console.log('no')}" 2>/dev/null)
167
169
 
168
170
  if [ "$START_SCRIPT" = "no" ]; then
169
171
  echo "[Smoke Tests] No start script in package.json — skipping smoke tests."
170
- else
172
+ exit 0
173
+ fi
171
174
 
172
- # ---------------------------------------------------------------
173
- # Step 1: Smoke Tests — start server + run tests
174
- # ---------------------------------------------------------------
175
- echo ""
176
- echo "[Smoke Tests] Starting server..."
177
-
178
- npm start &
179
- SERVER_PID=\$!
180
-
181
- # Auto-detect port — tries all common ports
182
- SERVER_UP=0
183
- for i in \$(seq 1 30); do
184
- for PORT_TRY in 3000 5000 8000 8080 4000 4200 3001 8081 1337 9000; do
185
- if curl -sf http://localhost:\$PORT_TRY > /dev/null 2>&1; then
186
- PORT=\$PORT_TRY
187
- SERVER_UP=1
188
- echo "[Smoke Tests] Server is up on port \$PORT."
189
- break 2
190
- fi
191
- done
192
- echo "[Smoke Tests] Waiting for server... (\$i/30)"
193
- sleep 1
175
+ # ---------------------------------------------------------------
176
+ # Smoke Tests — start server + run tests
177
+ # ---------------------------------------------------------------
178
+ echo ""
179
+ echo "[Smoke Tests] Starting server..."
180
+
181
+ npm start &
182
+ SERVER_PID=\$!
183
+
184
+ # Auto-detect port
185
+ SERVER_UP=0
186
+ for i in \$(seq 1 30); do
187
+ for PORT_TRY in 3000 5000 8000 8080 4000 4200 3001 8081 1337; do
188
+ if curl -sf http://localhost:\$PORT_TRY > /dev/null 2>&1; then
189
+ PORT=\$PORT_TRY
190
+ SERVER_UP=1
191
+ echo "[Smoke Tests] Server is up on port \$PORT."
192
+ break 2
193
+ fi
194
194
  done
195
+ echo "[Smoke Tests] Waiting for server... (\$i/30)"
196
+ sleep 1
197
+ done
195
198
 
196
- if [ \$SERVER_UP -eq 0 ]; then
197
- echo "[Smoke Tests] Server did not start in time. Aborting."
199
+ if [ \$SERVER_UP -eq 0 ]; then
200
+ echo "[Smoke Tests] Server did not start in time. Aborting."
201
+ kill \$SERVER_PID 2>/dev/null
202
+ exit 1
203
+ fi
204
+
205
+ if [ "$TEST_SCRIPT" = "no" ]; then
206
+ echo "[Smoke Tests] No test script in package.json — skipping npm test."
207
+ else
208
+ echo "[Smoke Tests] Running npm test..."
209
+ npm test
210
+ SMOKE_EXIT=\$?
211
+ if [ \$SMOKE_EXIT -ne 0 ]; then
198
212
  kill \$SERVER_PID 2>/dev/null
213
+ echo "[Smoke Tests] Failed. Push blocked."
199
214
  exit 1
200
215
  fi
216
+ echo "[Smoke Tests] Passed. ✔"
217
+ fi
201
218
 
202
- if [ "$TEST_SCRIPT" = "no" ]; then
203
- echo "[Smoke Tests] No test script in package.json — skipping npm test."
204
- else
205
- echo "[Smoke Tests] Running npm test..."
206
- npm test
207
- SMOKE_EXIT=\$?
208
-
209
- if [ \$SMOKE_EXIT -ne 0 ]; then
210
- kill \$SERVER_PID 2>/dev/null
211
- echo "[Smoke Tests] Failed. Push blocked."
212
- exit 1
213
- fi
214
- echo "[Smoke Tests] Passed. ✔"
215
- fi
216
-
217
- # ---------------------------------------------------------------
218
- # Step 2: Newman API Tests
219
- # ---------------------------------------------------------------
220
- echo ""
221
- echo "[Newman] Looking for Postman collections..."
222
-
223
- COLLECTIONS=\$(find . \\
224
- -not -path '*/node_modules/*' \\
225
- -not -path '*/.git/*' \\
226
- -not -path '*/scripts/*' \\
227
- \\( -name "*.postman_collection.json" -o -name "collection.json" \\) \\
228
- 2>/dev/null)
229
-
230
- if [ -z "\$COLLECTIONS" ]; then
231
- echo "[Newman] No Postman collection found. Skipping."
232
- kill \$SERVER_PID 2>/dev/null
233
- exit 0
234
- fi
219
+ # ---------------------------------------------------------------
220
+ # Newman API Tests
221
+ # ---------------------------------------------------------------
222
+ echo ""
223
+ echo "[Newman] Looking for Postman collections..."
235
224
 
236
- if ! command -v newman > /dev/null 2>&1; then
237
- echo "[Newman] Installing newman globally..."
238
- npm install -g newman newman-reporter-htmlextra 2>/dev/null || true
239
- fi
225
+ COLLECTIONS=\$(find . \\
226
+ -not -path '*/node_modules/*' \\
227
+ -not -path '*/.git/*' \\
228
+ -not -path '*/scripts/*' \\
229
+ \\( -name "*.postman_collection.json" -o -name "collection.json" \\) \\
230
+ 2>/dev/null)
240
231
 
241
- mkdir -p newman-reports
232
+ if [ -z "\$COLLECTIONS" ]; then
233
+ echo "[Newman] No Postman collection found. Skipping."
234
+ kill \$SERVER_PID 2>/dev/null
235
+ exit 0
236
+ fi
242
237
 
243
- ENV_FILE=\$(find . \\
244
- -not -path '*/node_modules/*' \\
245
- -not -path '*/.git/*' \\
246
- -name "*.postman_environment.json" \\
247
- 2>/dev/null | head -1)
238
+ if ! command -v newman > /dev/null 2>&1; then
239
+ echo "[Newman] Installing newman..."
240
+ npm install -g newman newman-reporter-htmlextra 2>/dev/null || true
241
+ fi
248
242
 
249
- NEWMAN_EXIT=0
250
- for COLLECTION in \$COLLECTIONS; do
251
- REPORT_NAME=\$(basename "\$COLLECTION" .json)
252
- echo "[Newman] Running: \$COLLECTION"
243
+ mkdir -p newman-reports
253
244
 
254
- ENV_FLAG=""
255
- if [ -n "\$ENV_FILE" ]; then
256
- ENV_FLAG="--environment \$ENV_FILE"
257
- fi
245
+ ENV_FILE=\$(find . \\
246
+ -not -path '*/node_modules/*' \\
247
+ -not -path '*/.git/*' \\
248
+ -name "*.postman_environment.json" \\
249
+ 2>/dev/null | head -1)
258
250
 
259
- newman run "\$COLLECTION" \\
260
- \$ENV_FLAG \\
261
- --env-var "baseUrl=http://localhost:\${PORT:-3000}" \\
262
- --reporters cli,htmlextra \\
263
- --reporter-htmlextra-export "newman-reports/\${REPORT_NAME}-report.html" \\
264
- --bail
251
+ NEWMAN_EXIT=0
252
+ for COLLECTION in \$COLLECTIONS; do
253
+ REPORT_NAME=\$(basename "\$COLLECTION" .json)
254
+ echo "[Newman] Running: \$COLLECTION"
265
255
 
266
- if [ \$? -ne 0 ]; then
267
- NEWMAN_EXIT=1
268
- fi
269
- done
256
+ ENV_FLAG=""
257
+ if [ -n "\$ENV_FILE" ]; then
258
+ ENV_FLAG="--environment \$ENV_FILE"
259
+ fi
270
260
 
271
- kill \$SERVER_PID 2>/dev/null
261
+ newman run "\$COLLECTION" \\
262
+ \$ENV_FLAG \\
263
+ --env-var "baseUrl=http://localhost:\${PORT:-3000}" \\
264
+ --reporters cli,htmlextra \\
265
+ --reporter-htmlextra-export "newman-reports/\${REPORT_NAME}-report.html" \\
266
+ --bail
272
267
 
273
- if [ \$NEWMAN_EXIT -ne 0 ]; then
274
- echo "[Newman] One or more collections failed. Push blocked."
275
- exit 1
268
+ if [ \$? -ne 0 ]; then
269
+ NEWMAN_EXIT=1
276
270
  fi
271
+ done
277
272
 
278
- echo "[Newman] All collections passed. ✔"
273
+ kill \$SERVER_PID 2>/dev/null
279
274
 
275
+ if [ \$NEWMAN_EXIT -ne 0 ]; then
276
+ echo "[Newman] One or more collections failed. Push blocked."
277
+ exit 1
280
278
  fi
281
279
 
280
+ echo "[Newman] All collections passed. ✔"
282
281
  exit 0
283
282
  `;
284
283
  }
package/lib/hooks.js CHANGED
@@ -11,8 +11,7 @@ exports.setupPreCommitHook = async (gitRoot) => {
11
11
  return;
12
12
  }
13
13
 
14
- // projectDir = where package.json, sonar-project.properties, .tools/ live
15
- // This is relative path from gitRoot to project (e.g. "server" or ".")
14
+ // relative path from gitRoot to project e.g. "server" or "."
16
15
  const projectDir = path.relative(gitRoot, process.cwd()) || '.';
17
16
 
18
17
  const hookContent = buildHookScript(projectDir);
@@ -26,7 +25,6 @@ exports.setupPreCommitHook = async (gitRoot) => {
26
25
  await fs.writeFile(hookPath, hookContent);
27
26
  await fs.chmod(hookPath, 0o755);
28
27
 
29
- // .gitleaksignore goes in the project dir, not git root
30
28
  const gitleaksIgnorePath = path.join(process.cwd(), '.gitleaksignore');
31
29
  await fs.writeFile(gitleaksIgnorePath, '.tools/\nsonar-project.properties\n');
32
30
  logInfo(".gitleaksignore created — excluding .tools/ and sonar-project.properties.");
@@ -35,22 +33,25 @@ exports.setupPreCommitHook = async (gitRoot) => {
35
33
  };
36
34
 
37
35
  function buildHookScript(projectDir) {
38
- // If project is in a subfolder, all tool paths must be prefixed
39
- const cdLine = projectDir !== '.' ? `cd "${projectDir}"` : '';
36
+ // All paths are relative to git root NO cd into subfolder
37
+ // git commands always run from git root where husky executes the hook
40
38
  const gitleaksBin = projectDir !== '.'
41
39
  ? `./${projectDir}/.tools/gitleaks/gitleaks`
42
40
  : `./.tools/gitleaks/gitleaks`;
41
+
43
42
  const sonarBin = projectDir !== '.'
44
43
  ? `./${projectDir}/node_modules/.bin/sonar-scanner`
45
44
  : `./node_modules/.bin/sonar-scanner`;
45
+
46
46
  const sonarProps = projectDir !== '.'
47
47
  ? `./${projectDir}/sonar-project.properties`
48
48
  : `./sonar-project.properties`;
49
49
 
50
50
  return `#!/bin/sh
51
51
 
52
- # Move to project directory if in monorepo/subfolder setup
53
- ${cdLine ? cdLine + '\n' : ''}
52
+ # Hook runs from git root all paths are relative to git root
53
+ # projectDir: ${projectDir}
54
+
54
55
  STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
55
56
 
56
57
  if [ -z "$STAGED_FILES" ]; then
@@ -75,9 +76,10 @@ else
75
76
 
76
77
  echo "$STAGED_FILES" | while IFS= read -r FILE; do
77
78
  case "$FILE" in
79
+ */sonar-project.properties) ;;
80
+ */.tools/*) ;;
78
81
  sonar-project.properties) ;;
79
82
  .tools/*) ;;
80
- ${projectDir !== '.' ? `${projectDir}/.tools/*) ;;` : ''}
81
83
  *)
82
84
  if [ -f "$FILE" ]; then
83
85
  DEST="$GITLEAKS_TMPDIR/$FILE"
@@ -128,7 +130,6 @@ else
128
130
 
129
131
  if [ $SONAR_EXIT -ne 0 ]; then
130
132
  echo "[SonarQube] Quality Gate FAILED. Commit blocked."
131
- echo "[SonarQube] Fix issues at: $SONAR_HOST"
132
133
  exit 1
133
134
  fi
134
135
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secure-husky-setup",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Automatic Husky + Gitleaks setup for any JS project",
5
5
  "main": "bin/index.js",
6
6
  "bin": {