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.
- package/lib/ci.js +103 -104
- package/lib/hooks.js +10 -9
- 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(
|
|
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 —
|
|
121
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
171
174
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
256
|
+
ENV_FLAG=""
|
|
257
|
+
if [ -n "\$ENV_FILE" ]; then
|
|
258
|
+
ENV_FLAG="--environment \$ENV_FILE"
|
|
259
|
+
fi
|
|
270
260
|
|
|
271
|
-
|
|
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 [
|
|
274
|
-
|
|
275
|
-
exit 1
|
|
268
|
+
if [ \$? -ne 0 ]; then
|
|
269
|
+
NEWMAN_EXIT=1
|
|
276
270
|
fi
|
|
271
|
+
done
|
|
277
272
|
|
|
278
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
39
|
-
|
|
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
|
-
#
|
|
53
|
-
|
|
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
|
|