secure-husky-setup 1.0.13 → 1.0.15
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 +30 -21
- 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,10 +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 ".")
|
|
16
14
|
const projectDir = path.relative(gitRoot, process.cwd()) || '.';
|
|
17
|
-
|
|
18
15
|
const hookContent = buildHookScript(projectDir);
|
|
19
16
|
|
|
20
17
|
if (await fs.pathExists(hookPath)) {
|
|
@@ -26,7 +23,6 @@ exports.setupPreCommitHook = async (gitRoot) => {
|
|
|
26
23
|
await fs.writeFile(hookPath, hookContent);
|
|
27
24
|
await fs.chmod(hookPath, 0o755);
|
|
28
25
|
|
|
29
|
-
// .gitleaksignore goes in the project dir, not git root
|
|
30
26
|
const gitleaksIgnorePath = path.join(process.cwd(), '.gitleaksignore');
|
|
31
27
|
await fs.writeFile(gitleaksIgnorePath, '.tools/\nsonar-project.properties\n');
|
|
32
28
|
logInfo(".gitleaksignore created — excluding .tools/ and sonar-project.properties.");
|
|
@@ -35,22 +31,34 @@ exports.setupPreCommitHook = async (gitRoot) => {
|
|
|
35
31
|
};
|
|
36
32
|
|
|
37
33
|
function buildHookScript(projectDir) {
|
|
38
|
-
//
|
|
39
|
-
|
|
34
|
+
// All paths relative to git root — NO cd at top level
|
|
35
|
+
// sonar-scanner runs in a subshell cd'd into project dir so it finds sonar-project.properties
|
|
40
36
|
const gitleaksBin = projectDir !== '.'
|
|
41
37
|
? `./${projectDir}/.tools/gitleaks/gitleaks`
|
|
42
38
|
: `./.tools/gitleaks/gitleaks`;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
|
|
40
|
+
// subshell cd for sonar so properties file is found correctly
|
|
41
|
+
const sonarSubshell = projectDir !== '.'
|
|
42
|
+
? `(cd "./${projectDir}" && ./node_modules/.bin/sonar-scanner -Dsonar.qualitygate.wait=true)`
|
|
43
|
+
: `(./node_modules/.bin/sonar-scanner -Dsonar.qualitygate.wait=true)`;
|
|
44
|
+
|
|
45
|
+
const sonarPropsCheck = projectDir !== '.'
|
|
47
46
|
? `./${projectDir}/sonar-project.properties`
|
|
48
47
|
: `./sonar-project.properties`;
|
|
49
48
|
|
|
49
|
+
const sonarBinCheck = projectDir !== '.'
|
|
50
|
+
? `./${projectDir}/node_modules/.bin/sonar-scanner`
|
|
51
|
+
: `./node_modules/.bin/sonar-scanner`;
|
|
52
|
+
|
|
53
|
+
const sonarHostGrep = projectDir !== '.'
|
|
54
|
+
? `grep "^sonar.host.url=" "./${projectDir}/sonar-project.properties"`
|
|
55
|
+
: `grep "^sonar.host.url=" "./sonar-project.properties"`;
|
|
56
|
+
|
|
50
57
|
return `#!/bin/sh
|
|
51
58
|
|
|
52
|
-
#
|
|
53
|
-
|
|
59
|
+
# Hook runs from git root — all paths relative to git root
|
|
60
|
+
# projectDir: ${projectDir}
|
|
61
|
+
|
|
54
62
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
55
63
|
|
|
56
64
|
if [ -z "$STAGED_FILES" ]; then
|
|
@@ -75,9 +83,10 @@ else
|
|
|
75
83
|
|
|
76
84
|
echo "$STAGED_FILES" | while IFS= read -r FILE; do
|
|
77
85
|
case "$FILE" in
|
|
86
|
+
*/sonar-project.properties) ;;
|
|
87
|
+
*/.tools/*) ;;
|
|
78
88
|
sonar-project.properties) ;;
|
|
79
89
|
.tools/*) ;;
|
|
80
|
-
${projectDir !== '.' ? `${projectDir}/.tools/*) ;;` : ''}
|
|
81
90
|
*)
|
|
82
91
|
if [ -f "$FILE" ]; then
|
|
83
92
|
DEST="$GITLEAKS_TMPDIR/$FILE"
|
|
@@ -103,16 +112,16 @@ fi
|
|
|
103
112
|
echo ""
|
|
104
113
|
echo "[SonarQube] Scanning changed files..."
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
SONAR_BIN_CHECK="${sonarBinCheck}"
|
|
116
|
+
SONAR_PROPS_CHECK="${sonarPropsCheck}"
|
|
108
117
|
|
|
109
|
-
if [ ! -f "$
|
|
118
|
+
if [ ! -f "$SONAR_BIN_CHECK" ]; then
|
|
110
119
|
echo "[SonarQube] sonar-scanner not found. Skipping."
|
|
111
120
|
else
|
|
112
|
-
if [ ! -f "$
|
|
121
|
+
if [ ! -f "$SONAR_PROPS_CHECK" ]; then
|
|
113
122
|
echo "[SonarQube] sonar-project.properties not found. Skipping."
|
|
114
123
|
else
|
|
115
|
-
SONAR_HOST=$(
|
|
124
|
+
SONAR_HOST=$(${sonarHostGrep} | cut -d'=' -f2 | tr -d '[:space:]')
|
|
116
125
|
SONAR_DOMAIN=$(echo "$SONAR_HOST" | sed 's|https://||' | sed 's|http://||' | cut -d'/' -f1 | cut -d':' -f1)
|
|
117
126
|
SONAR_PORT=$(echo "$SONAR_HOST" | grep -o ':[0-9]*$' | tr -d ':')
|
|
118
127
|
SONAR_PORT=\${SONAR_PORT:-9000}
|
|
@@ -123,12 +132,12 @@ else
|
|
|
123
132
|
SONAR_INCLUSIONS=$(echo "$STAGED_FILES" | tr '\\n' ',' | sed 's/,$//')
|
|
124
133
|
echo "[SonarQube] Scanning: $SONAR_INCLUSIONS"
|
|
125
134
|
|
|
126
|
-
|
|
135
|
+
# Run sonar-scanner in subshell from project dir so it picks up sonar-project.properties
|
|
136
|
+
${sonarSubshell} -Dsonar.inclusions="\$SONAR_INCLUSIONS"
|
|
127
137
|
SONAR_EXIT=$?
|
|
128
138
|
|
|
129
|
-
if [
|
|
139
|
+
if [ \$SONAR_EXIT -ne 0 ]; then
|
|
130
140
|
echo "[SonarQube] Quality Gate FAILED. Commit blocked."
|
|
131
|
-
echo "[SonarQube] Fix issues at: $SONAR_HOST"
|
|
132
141
|
exit 1
|
|
133
142
|
fi
|
|
134
143
|
|