secure-husky-setup 1.0.0
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/Readme.md +49 -0
- package/bin/index.js +62 -0
- package/lib/ci.js +323 -0
- package/lib/git.js +7 -0
- package/lib/gitleaks.js +111 -0
- package/lib/hooks.js +112 -0
- package/lib/husky.js +27 -0
- package/lib/logger.js +5 -0
- package/lib/packageManager.js +5 -0
- package/lib/sonarqube.js +102 -0
- package/lib/utils.js +13 -0
- package/package.json +24 -0
- package/templates/ci-tests.yml +163 -0
package/Readme.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
```markdown
|
|
5
|
+
# Secure Husky Setup
|
|
6
|
+
|
|
7
|
+
Automatically installs and configures:
|
|
8
|
+
|
|
9
|
+
- Husky (Git hooks)
|
|
10
|
+
- Gitleaks (Secret scanning)
|
|
11
|
+
- Pre-commit protection
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Install from GitHub
|
|
16
|
+
|
|
17
|
+
Inside your project directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install --save-dev git+https://github.com/HUSAINTRIVEDI52/npm-package-husky-gitleaks.git
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Initialize
|
|
27
|
+
|
|
28
|
+
After installing, run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx secure-husky-setup init
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This will:
|
|
35
|
+
|
|
36
|
+
- Install Husky locally
|
|
37
|
+
- Download Gitleaks locally
|
|
38
|
+
- Configure the pre-commit hook
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Done
|
|
43
|
+
|
|
44
|
+
Now every `git commit` will automatically scan for secrets.
|
|
45
|
+
|
|
46
|
+
If secrets are detected, the commit will be blocked.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { installHusky } = require('../lib/husky');
|
|
3
|
+
const { installGitleaks } = require('../lib/gitleaks');
|
|
4
|
+
const { installSonarScanner, setupSonarProperties } = require('../lib/sonarqube');
|
|
5
|
+
const { setupPreCommitHook } = require('../lib/hooks');
|
|
6
|
+
// Added by Arjun — import CI setup functions from the new lib/ci.js module
|
|
7
|
+
const { setupPrePushHook, setupCIScript, setupCIWorkflow, validateProject, ensurePackageLock } = require('../lib/ci');
|
|
8
|
+
const { isGitRepo } = require('../lib/git');
|
|
9
|
+
const { logInfo, logError, logSuccess } = require('../lib/logger');
|
|
10
|
+
|
|
11
|
+
const command = process.argv[2];
|
|
12
|
+
|
|
13
|
+
(async () => {
|
|
14
|
+
if (command !== 'init') {
|
|
15
|
+
console.log("Usage: secure-husky-setup init");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
logInfo("Initializing secure git hooks...");
|
|
21
|
+
|
|
22
|
+
if (!await isGitRepo()) {
|
|
23
|
+
throw new Error("Not inside a git repository.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Existing steps — pre-commit hooks (no changes made here) ─────────────
|
|
27
|
+
await installHusky();
|
|
28
|
+
await installGitleaks();
|
|
29
|
+
await installSonarScanner();
|
|
30
|
+
await setupSonarProperties();
|
|
31
|
+
await setupPreCommitHook();
|
|
32
|
+
|
|
33
|
+
logSuccess("Secure Husky + Gitleaks + SonarQube setup completed.");
|
|
34
|
+
logInfo("Next step: edit sonar-project.properties and set sonar.host.url and sonar.token.");
|
|
35
|
+
|
|
36
|
+
// Added by Arjun — pre-push hook + GitHub Actions CI workflow setup ───────
|
|
37
|
+
// Runs Newman API tests and smoke tests automatically on every git push
|
|
38
|
+
logInfo("Setting up Newman & Smoke Test CI workflow...");
|
|
39
|
+
|
|
40
|
+
// Added by Arjun — ensure package-lock.json exists (required by npm ci in workflow)
|
|
41
|
+
await ensurePackageLock();
|
|
42
|
+
|
|
43
|
+
// Added by Arjun — validate package.json has "start" and "test" scripts
|
|
44
|
+
await validateProject();
|
|
45
|
+
|
|
46
|
+
// Added by Arjun — write standalone scripts/run-ci-checks.sh (all test logic lives here)
|
|
47
|
+
await setupCIScript();
|
|
48
|
+
|
|
49
|
+
// Added by Arjun — copy ci-tests.yml into .github/workflows/
|
|
50
|
+
await setupCIWorkflow();
|
|
51
|
+
|
|
52
|
+
// Added by Arjun — create .husky/pre-push hook (thin wrapper that calls run-ci-checks.sh)
|
|
53
|
+
await setupPrePushHook();
|
|
54
|
+
|
|
55
|
+
logSuccess("Newman + Smoke Test pre-push hook and GitHub Actions workflow setup completed.");
|
|
56
|
+
// ── End of Arjun's additions ──────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
} catch (err) {
|
|
59
|
+
logError(err.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
})();
|
package/lib/ci.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Added by Arjun
|
|
5
|
+
// File: lib/ci.js
|
|
6
|
+
// Purpose: Sets up Newman API tests + Smoke Tests as a pre-push git hook
|
|
7
|
+
// and copies the GitHub Actions CI workflow into the project.
|
|
8
|
+
//
|
|
9
|
+
// FLEXIBILITY NOTE:
|
|
10
|
+
// Test logic lives in scripts/run-ci-checks.sh (standalone script).
|
|
11
|
+
// The pre-push hook simply calls that script.
|
|
12
|
+
// To move tests to pre-commit in future, just add one line to pre-commit hook:
|
|
13
|
+
// ./scripts/run-ci-checks.sh
|
|
14
|
+
// No logic needs to be rewritten.
|
|
15
|
+
//
|
|
16
|
+
// New files introduced:
|
|
17
|
+
// - lib/ci.js (this file)
|
|
18
|
+
// - templates/ci-tests.yml (GitHub Actions workflow template)
|
|
19
|
+
// Changes made to existing files:
|
|
20
|
+
// - bin/index.js (4 new lines added — see comments there)
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const fs = require('fs-extra');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { execSync } = require('child_process');
|
|
26
|
+
const { logInfo, logSuccess, logError } = require('./logger');
|
|
27
|
+
|
|
28
|
+
// Added by Arjun — path to the CI workflow template bundled with this package
|
|
29
|
+
const TEMPLATE_PATH = path.resolve(__dirname, '../templates/ci-tests.yml');
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Added by Arjun — writes scripts/run-ci-checks.sh into the project
|
|
33
|
+
// This is the STANDALONE script containing all test logic.
|
|
34
|
+
// It can be called from pre-push, pre-commit, or any other hook.
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
exports.setupCIScript = async () => {
|
|
37
|
+
const scriptsDir = path.join(process.cwd(), 'scripts');
|
|
38
|
+
const scriptPath = path.join(scriptsDir, 'run-ci-checks.sh');
|
|
39
|
+
|
|
40
|
+
await fs.ensureDir(scriptsDir);
|
|
41
|
+
|
|
42
|
+
if (await fs.pathExists(scriptPath)) {
|
|
43
|
+
logInfo("run-ci-checks.sh already exists — overwriting with latest version.");
|
|
44
|
+
} else {
|
|
45
|
+
logInfo("Creating scripts/run-ci-checks.sh...");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await fs.writeFile(scriptPath, buildCIScript());
|
|
49
|
+
await fs.chmod(scriptPath, 0o755);
|
|
50
|
+
logSuccess("scripts/run-ci-checks.sh created.");
|
|
51
|
+
logInfo("To move tests to pre-commit in future: add './scripts/run-ci-checks.sh' to .husky/pre-commit.");
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Added by Arjun — sets up .husky/pre-push hook
|
|
56
|
+
// Simply calls run-ci-checks.sh — no logic lives here.
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
exports.setupPrePushHook = async () => {
|
|
59
|
+
const huskyDir = path.join(process.cwd(), '.husky');
|
|
60
|
+
const hookPath = path.join(huskyDir, 'pre-push');
|
|
61
|
+
|
|
62
|
+
if (!await fs.pathExists(huskyDir)) {
|
|
63
|
+
logInfo("Husky directory not found. Skipping pre-push hook setup.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (await fs.pathExists(hookPath)) {
|
|
68
|
+
logInfo("Pre-push hook already configured. Overwriting with latest setup...");
|
|
69
|
+
} else {
|
|
70
|
+
logInfo("Creating new pre-push hook...");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await fs.writeFile(hookPath, buildPrePushHook());
|
|
74
|
+
await fs.chmod(hookPath, 0o755);
|
|
75
|
+
logSuccess("Pre-push hook created — calls scripts/run-ci-checks.sh.");
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// Added by Arjun — copies ci-tests.yml into .github/workflows/
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
exports.setupCIWorkflow = async () => {
|
|
82
|
+
const targetDir = path.join(process.cwd(), '.github', 'workflows');
|
|
83
|
+
const targetFile = path.join(targetDir, 'ci-tests.yml');
|
|
84
|
+
|
|
85
|
+
if (!await fs.pathExists(TEMPLATE_PATH)) {
|
|
86
|
+
logError("CI template not found. Please reinstall the package.");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await fs.ensureDir(targetDir);
|
|
91
|
+
|
|
92
|
+
if (await fs.pathExists(targetFile)) {
|
|
93
|
+
logInfo("ci-tests.yml already exists — overwriting with latest version.");
|
|
94
|
+
} else {
|
|
95
|
+
logInfo("Creating .github/workflows/ci-tests.yml...");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await fs.copy(TEMPLATE_PATH, targetFile);
|
|
99
|
+
logSuccess("GitHub Actions workflow copied to .github/workflows/ci-tests.yml");
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// Added by Arjun — validates package.json has required scripts
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
exports.validateProject = async () => {
|
|
106
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
107
|
+
|
|
108
|
+
if (!await fs.pathExists(pkgPath)) {
|
|
109
|
+
logError("No package.json found. Skipping validation.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
114
|
+
const scripts = pkg.scripts || {};
|
|
115
|
+
|
|
116
|
+
if (!scripts.start) {
|
|
117
|
+
logError('No "start" script in package.json — CI server boot will fail.');
|
|
118
|
+
logInfo('Add: "start": "node index.js"');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!scripts.test) {
|
|
122
|
+
logError('No "test" script in package.json — smoke tests will fail.');
|
|
123
|
+
logInfo('Add: "test": "jest" (or your test runner)');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (scripts.start && scripts.test) {
|
|
127
|
+
logSuccess('package.json has required "start" and "test" scripts.');
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
// Added by Arjun — ensures package-lock.json exists (required by npm ci)
|
|
133
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
exports.ensurePackageLock = async () => {
|
|
135
|
+
const lockPath = path.join(process.cwd(), 'package-lock.json');
|
|
136
|
+
const yarnPath = path.join(process.cwd(), 'yarn.lock');
|
|
137
|
+
|
|
138
|
+
if (await fs.pathExists(lockPath) || await fs.pathExists(yarnPath)) {
|
|
139
|
+
logSuccess("Lock file found (package-lock.json / yarn.lock).");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logInfo("No package-lock.json found — running npm install to generate it...");
|
|
144
|
+
try {
|
|
145
|
+
execSync('npm install', { stdio: 'inherit', cwd: process.cwd() });
|
|
146
|
+
logSuccess("package-lock.json generated. Remember to commit it.");
|
|
147
|
+
} catch {
|
|
148
|
+
logError("Failed to generate package-lock.json. Run npm install manually.");
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
153
|
+
// Pre-push hook — thin wrapper, just calls the standalone script
|
|
154
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
155
|
+
function buildPrePushHook() {
|
|
156
|
+
return `#!/bin/sh
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------
|
|
159
|
+
# Pre-push hook — Newman + Smoke Tests
|
|
160
|
+
# Delegates all logic to scripts/run-ci-checks.sh
|
|
161
|
+
#
|
|
162
|
+
# To move tests to pre-commit in future:
|
|
163
|
+
# Remove this file and add this line to .husky/pre-commit:
|
|
164
|
+
# ./scripts/run-ci-checks.sh
|
|
165
|
+
# ---------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
./scripts/run-ci-checks.sh
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
+
// Standalone CI checks script — ALL test logic lives here
|
|
173
|
+
// Can be called from pre-push, pre-commit, CI, or manually:
|
|
174
|
+
// sh scripts/run-ci-checks.sh
|
|
175
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
176
|
+
function buildCIScript() {
|
|
177
|
+
return `#!/bin/sh
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------
|
|
180
|
+
# run-ci-checks.sh — Smoke Tests + Newman API Tests
|
|
181
|
+
#
|
|
182
|
+
# Called by .husky/pre-push by default.
|
|
183
|
+
# To move to pre-commit: add './scripts/run-ci-checks.sh' to .husky/pre-commit
|
|
184
|
+
# To run manually: sh scripts/run-ci-checks.sh
|
|
185
|
+
# ---------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------
|
|
188
|
+
# Git diff check — only run if actual files changed
|
|
189
|
+
# ---------------------------------------------------------------
|
|
190
|
+
LOCAL=$(git rev-parse @)
|
|
191
|
+
REMOTE=$(git rev-parse @{u} 2>/dev/null)
|
|
192
|
+
|
|
193
|
+
if [ "$REMOTE" != "" ] && [ "$LOCAL" = "$REMOTE" ]; then
|
|
194
|
+
echo "[CI Checks] No changes to push. Skipping."
|
|
195
|
+
exit 0
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
if [ "$REMOTE" != "" ]; then
|
|
199
|
+
CHANGED=$(git diff --name-only "$REMOTE" "$LOCAL" 2>/dev/null)
|
|
200
|
+
else
|
|
201
|
+
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null)
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
if [ -z "$CHANGED" ]; then
|
|
205
|
+
echo "[CI Checks] No changed files detected. Skipping."
|
|
206
|
+
exit 0
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
echo ""
|
|
210
|
+
echo "[CI Checks] Changed files detected:"
|
|
211
|
+
echo "$CHANGED" | sed 's/^/ -> /'
|
|
212
|
+
|
|
213
|
+
echo ""
|
|
214
|
+
echo "[CI Checks] Starting checks..."
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------
|
|
217
|
+
# Step 1: Smoke Tests
|
|
218
|
+
# ---------------------------------------------------------------
|
|
219
|
+
echo ""
|
|
220
|
+
echo "[Smoke Tests] Starting server..."
|
|
221
|
+
|
|
222
|
+
npm start &
|
|
223
|
+
SERVER_PID=\$!
|
|
224
|
+
|
|
225
|
+
for i in \$(seq 1 30); do
|
|
226
|
+
if curl -sf http://localhost:\${PORT:-3000}/health > /dev/null 2>&1 || \\
|
|
227
|
+
curl -sf http://localhost:\${PORT:-3000} > /dev/null 2>&1; then
|
|
228
|
+
echo "[Smoke Tests] Server is up."
|
|
229
|
+
break
|
|
230
|
+
fi
|
|
231
|
+
echo "[Smoke Tests] Waiting for server... (\$i/30)"
|
|
232
|
+
sleep 1
|
|
233
|
+
done
|
|
234
|
+
|
|
235
|
+
echo "[Smoke Tests] Running npm test..."
|
|
236
|
+
npm test
|
|
237
|
+
SMOKE_EXIT=\$?
|
|
238
|
+
|
|
239
|
+
kill \$SERVER_PID 2>/dev/null
|
|
240
|
+
|
|
241
|
+
if [ \$SMOKE_EXIT -ne 0 ]; then
|
|
242
|
+
echo "[Smoke Tests] Failed. Push blocked."
|
|
243
|
+
exit 1
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
echo "[Smoke Tests] Passed. ✔"
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------
|
|
249
|
+
# Step 2: Newman
|
|
250
|
+
# ---------------------------------------------------------------
|
|
251
|
+
echo ""
|
|
252
|
+
echo "[Newman] Looking for Postman collections..."
|
|
253
|
+
|
|
254
|
+
COLLECTIONS=\$(find . \\
|
|
255
|
+
-not -path '*/node_modules/*' \\
|
|
256
|
+
-not -path '*/.git/*' \\
|
|
257
|
+
-not -path '*/scripts/*' \\
|
|
258
|
+
\\( -name "*.postman_collection.json" -o -name "collection.json" \\) \\
|
|
259
|
+
2>/dev/null)
|
|
260
|
+
|
|
261
|
+
if [ -z "\$COLLECTIONS" ]; then
|
|
262
|
+
echo "[Newman] No Postman collection found. Skipping."
|
|
263
|
+
exit 0
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
if ! command -v newman > /dev/null 2>&1; then
|
|
267
|
+
echo "[Newman] Installing newman globally..."
|
|
268
|
+
npm install -g newman newman-reporter-htmlextra
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
npm start &
|
|
272
|
+
SERVER_PID=\$!
|
|
273
|
+
|
|
274
|
+
for i in \$(seq 1 30); do
|
|
275
|
+
if curl -sf http://localhost:\${PORT:-3000}/health > /dev/null 2>&1 || \\
|
|
276
|
+
curl -sf http://localhost:\${PORT:-3000} > /dev/null 2>&1; then
|
|
277
|
+
echo "[Newman] Server is up."
|
|
278
|
+
break
|
|
279
|
+
fi
|
|
280
|
+
sleep 1
|
|
281
|
+
done
|
|
282
|
+
|
|
283
|
+
mkdir -p newman-reports
|
|
284
|
+
|
|
285
|
+
ENV_FILE=\$(find . \\
|
|
286
|
+
-not -path '*/node_modules/*' \\
|
|
287
|
+
-not -path '*/.git/*' \\
|
|
288
|
+
-name "*.postman_environment.json" \\
|
|
289
|
+
2>/dev/null | head -1)
|
|
290
|
+
|
|
291
|
+
NEWMAN_EXIT=0
|
|
292
|
+
for COLLECTION in \$COLLECTIONS; do
|
|
293
|
+
REPORT_NAME=\$(basename "\$COLLECTION" .json)
|
|
294
|
+
echo "[Newman] Running: \$COLLECTION"
|
|
295
|
+
|
|
296
|
+
ENV_FLAG=""
|
|
297
|
+
if [ -n "\$ENV_FILE" ]; then
|
|
298
|
+
ENV_FLAG="--environment \$ENV_FILE"
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
newman run "\$COLLECTION" \\
|
|
302
|
+
\$ENV_FLAG \\
|
|
303
|
+
--env-var "baseUrl=http://localhost:\${PORT:-3000}" \\
|
|
304
|
+
--reporters cli,htmlextra \\
|
|
305
|
+
--reporter-htmlextra-export "newman-reports/\${REPORT_NAME}-report.html" \\
|
|
306
|
+
--bail
|
|
307
|
+
|
|
308
|
+
if [ \$? -ne 0 ]; then
|
|
309
|
+
NEWMAN_EXIT=1
|
|
310
|
+
fi
|
|
311
|
+
done
|
|
312
|
+
|
|
313
|
+
kill \$SERVER_PID 2>/dev/null
|
|
314
|
+
|
|
315
|
+
if [ \$NEWMAN_EXIT -ne 0 ]; then
|
|
316
|
+
echo "[Newman] One or more collections failed. Push blocked."
|
|
317
|
+
exit 1
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
echo "[Newman] All collections passed. ✔"
|
|
321
|
+
exit 0
|
|
322
|
+
`;
|
|
323
|
+
}
|
package/lib/git.js
ADDED
package/lib/gitleaks.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const execa = require('execa');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const { logInfo, logSuccess } = require('./logger');
|
|
6
|
+
|
|
7
|
+
const VERSION = "8.18.0";
|
|
8
|
+
|
|
9
|
+
// Added by Arjun — detect the correct gitleaks binary for the current OS and architecture
|
|
10
|
+
function getPlatformAsset() {
|
|
11
|
+
const platform = process.platform;
|
|
12
|
+
const arch = process.arch;
|
|
13
|
+
|
|
14
|
+
const archMap = {
|
|
15
|
+
x64: 'x64',
|
|
16
|
+
arm64: 'arm64',
|
|
17
|
+
arm: 'armv7',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const gitleaksArch = archMap[arch] || 'x64';
|
|
21
|
+
|
|
22
|
+
if (platform === 'darwin') {
|
|
23
|
+
return { filename: `gitleaks_${VERSION}_darwin_${gitleaksArch}.tar.gz`, extract: 'tar' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (platform === 'win32') {
|
|
27
|
+
return { filename: `gitleaks_${VERSION}_windows_${gitleaksArch}.zip`, extract: 'zip' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { filename: `gitleaks_${VERSION}_linux_${gitleaksArch}.tar.gz`, extract: 'tar' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Added by Arjun — automatically add entries to .gitignore if missing
|
|
34
|
+
async function ensureGitignoreEntries(entries) {
|
|
35
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
36
|
+
|
|
37
|
+
let content = '';
|
|
38
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
39
|
+
content = await fs.readFile(gitignorePath, 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const added = [];
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (!content.includes(entry)) {
|
|
45
|
+
content += `\n${entry}`;
|
|
46
|
+
added.push(entry);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (added.length > 0) {
|
|
51
|
+
await fs.writeFile(gitignorePath, content);
|
|
52
|
+
logInfo(`.gitignore updated — added: ${added.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
exports.installGitleaks = async () => {
|
|
57
|
+
const toolsDir = path.join(process.cwd(), '.tools');
|
|
58
|
+
const gitleaksDir = path.join(toolsDir, 'gitleaks');
|
|
59
|
+
const binaryPath = path.join(gitleaksDir, 'gitleaks');
|
|
60
|
+
|
|
61
|
+
if (await fs.pathExists(binaryPath)) {
|
|
62
|
+
logInfo("Gitleaks already installed locally.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logInfo("Installing Gitleaks locally...");
|
|
67
|
+
await fs.ensureDir(gitleaksDir);
|
|
68
|
+
|
|
69
|
+
// Added by Arjun — use platform-aware asset instead of hardcoded linux_x64
|
|
70
|
+
const { filename, extract } = getPlatformAsset();
|
|
71
|
+
const url = `https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/${filename}`;
|
|
72
|
+
const destPath = path.join(gitleaksDir, filename);
|
|
73
|
+
|
|
74
|
+
logInfo(`Downloading ${filename}...`);
|
|
75
|
+
await downloadFile(url, destPath);
|
|
76
|
+
|
|
77
|
+
// Added by Arjun — handle both tar.gz (mac/linux) and zip (windows)
|
|
78
|
+
if (extract === 'tar') {
|
|
79
|
+
await execa('tar', ['-xzf', destPath, '-C', gitleaksDir]);
|
|
80
|
+
} else {
|
|
81
|
+
await execa('unzip', ['-o', destPath, '-d', gitleaksDir]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await fs.remove(destPath);
|
|
85
|
+
await fs.chmod(binaryPath, 0o755);
|
|
86
|
+
|
|
87
|
+
// Added by Arjun — automatically add .tools/ and node_modules/ to .gitignore
|
|
88
|
+
// .tools/ → prevents gitleaks binary from being staged and scanned
|
|
89
|
+
// node_modules/ → prevents dependencies from being committed to git
|
|
90
|
+
await ensureGitignoreEntries(['.tools/', 'node_modules/']);
|
|
91
|
+
|
|
92
|
+
logSuccess("Gitleaks installed locally.");
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function downloadFile(url, dest) {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const request = https.get(url, { headers: { 'User-Agent': 'node' } }, (response) => {
|
|
98
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
99
|
+
return downloadFile(response.headers.location, dest).then(resolve).catch(reject);
|
|
100
|
+
}
|
|
101
|
+
if (response.statusCode !== 200) {
|
|
102
|
+
return reject(new Error(`Download failed with status code ${response.statusCode}`));
|
|
103
|
+
}
|
|
104
|
+
const file = fs.createWriteStream(dest);
|
|
105
|
+
response.pipe(file);
|
|
106
|
+
file.on('finish', () => file.close(resolve));
|
|
107
|
+
file.on('error', reject);
|
|
108
|
+
});
|
|
109
|
+
request.on('error', reject);
|
|
110
|
+
});
|
|
111
|
+
}
|
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { logInfo, logSuccess } = require('./logger');
|
|
4
|
+
|
|
5
|
+
exports.setupPreCommitHook = async () => {
|
|
6
|
+
const huskyDir = path.join(process.cwd(), '.husky');
|
|
7
|
+
const hookPath = path.join(huskyDir, 'pre-commit');
|
|
8
|
+
|
|
9
|
+
if (!await fs.pathExists(huskyDir)) {
|
|
10
|
+
logInfo("Husky directory not found. Skipping hook setup.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hookContent = buildHookScript();
|
|
15
|
+
|
|
16
|
+
if (await fs.pathExists(hookPath)) {
|
|
17
|
+
logInfo("Pre-commit hook already configured. Overwriting with latest setup...");
|
|
18
|
+
} else {
|
|
19
|
+
logInfo("Creating new pre-commit hook...");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await fs.writeFile(hookPath, hookContent);
|
|
23
|
+
await fs.chmod(hookPath, 0o755);
|
|
24
|
+
|
|
25
|
+
const gitleaksIgnorePath = path.join(process.cwd(), '.gitleaksignore');
|
|
26
|
+
await fs.writeFile(gitleaksIgnorePath, '.tools/\nsonar-project.properties\n');
|
|
27
|
+
logInfo(".gitleaksignore created — excluding .tools/ and sonar-project.properties.");
|
|
28
|
+
|
|
29
|
+
logSuccess("Pre-commit hook created with Gitleaks + SonarQube (git diff only).");
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function buildHookScript() {
|
|
33
|
+
return `#!/bin/sh
|
|
34
|
+
|
|
35
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
36
|
+
|
|
37
|
+
if [ -z "$STAGED_FILES" ]; then
|
|
38
|
+
echo "No changed files detected. Skipping checks."
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "[Git Diff] Changed files in this commit:"
|
|
43
|
+
echo "$STAGED_FILES" | while IFS= read -r FILE; do
|
|
44
|
+
echo " -> $FILE"
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
echo ""
|
|
48
|
+
echo "[Gitleaks] Scanning changed files for secrets..."
|
|
49
|
+
|
|
50
|
+
GITLEAKS_BIN="./.tools/gitleaks/gitleaks"
|
|
51
|
+
|
|
52
|
+
if [ ! -f "$GITLEAKS_BIN" ]; then
|
|
53
|
+
echo "[Gitleaks] Binary not found. Skipping."
|
|
54
|
+
else
|
|
55
|
+
GITLEAKS_TMPDIR=$(mktemp -d)
|
|
56
|
+
|
|
57
|
+
echo "$STAGED_FILES" | while IFS= read -r FILE; do
|
|
58
|
+
case "$FILE" in
|
|
59
|
+
sonar-project.properties) ;;
|
|
60
|
+
.tools/*) ;;
|
|
61
|
+
*)
|
|
62
|
+
if [ -f "$FILE" ]; then
|
|
63
|
+
DEST="$GITLEAKS_TMPDIR/$FILE"
|
|
64
|
+
mkdir -p "$(dirname "$DEST")"
|
|
65
|
+
cp "$FILE" "$DEST"
|
|
66
|
+
fi
|
|
67
|
+
;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
$GITLEAKS_BIN detect --source "$GITLEAKS_TMPDIR" --no-git --verbose
|
|
72
|
+
GITLEAKS_EXIT=$?
|
|
73
|
+
rm -rf "$GITLEAKS_TMPDIR"
|
|
74
|
+
|
|
75
|
+
if [ $GITLEAKS_EXIT -ne 0 ]; then
|
|
76
|
+
echo "[Gitleaks] Secrets detected! Commit blocked."
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
echo "[Gitleaks] No secrets found."
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
echo ""
|
|
84
|
+
echo "[SonarQube] Scanning changed files..."
|
|
85
|
+
|
|
86
|
+
SONAR_BIN="./node_modules/.bin/sonar-scanner"
|
|
87
|
+
|
|
88
|
+
if [ ! -f "$SONAR_BIN" ]; then
|
|
89
|
+
echo "[SonarQube] sonar-scanner not found. Skipping."
|
|
90
|
+
else
|
|
91
|
+
if [ ! -f "sonar-project.properties" ]; then
|
|
92
|
+
echo "[SonarQube] sonar-project.properties not found. Skipping."
|
|
93
|
+
else
|
|
94
|
+
SONAR_INCLUSIONS=$(echo "$STAGED_FILES" | tr '\n' ',' | sed 's/,$//')
|
|
95
|
+
echo "[SonarQube] Scanning: $SONAR_INCLUSIONS"
|
|
96
|
+
|
|
97
|
+
$SONAR_BIN -Dsonar.inclusions="$SONAR_INCLUSIONS" -Dsonar.qualitygate.wait=true
|
|
98
|
+
SONAR_EXIT=$?
|
|
99
|
+
|
|
100
|
+
if [ $SONAR_EXIT -ne 0 ]; then
|
|
101
|
+
echo "[SonarQube] Quality Gate FAILED. Commit blocked."
|
|
102
|
+
echo "[SonarQube] Fix the issues at: $(grep 'sonar.host.url' sonar-project.properties | cut -d'=' -f2)/dashboard?id=$(grep 'sonar.projectKey' sonar-project.properties | cut -d'=' -f2)"
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
echo "[SonarQube] Quality Gate PASSED. ✔"
|
|
107
|
+
fi
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
exit 0
|
|
111
|
+
`;
|
|
112
|
+
}
|
package/lib/husky.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { fileExists, readJSON, writeJSON } = require('./utils');
|
|
2
|
+
const { installDevDependency } = require('./packageManager');
|
|
3
|
+
const execa = require('execa');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const { logInfo, logSuccess } = require('./logger');
|
|
7
|
+
|
|
8
|
+
exports.installHusky = async () => {
|
|
9
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
10
|
+
const pkg = await readJSON(pkgPath);
|
|
11
|
+
|
|
12
|
+
if (!pkg.devDependencies || !pkg.devDependencies.husky) {
|
|
13
|
+
logInfo("Installing Husky...");
|
|
14
|
+
await installDevDependency('husky');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
logInfo("Initializing Husky...");
|
|
18
|
+
await execa('npx', ['husky', 'install'], { stdio: 'inherit' });
|
|
19
|
+
|
|
20
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
21
|
+
|
|
22
|
+
if (!pkg.scripts.prepare) {
|
|
23
|
+
pkg.scripts.prepare = "husky install";
|
|
24
|
+
await writeJSON(pkgPath, pkg);
|
|
25
|
+
logSuccess("Added prepare script.");
|
|
26
|
+
}
|
|
27
|
+
};
|
package/lib/logger.js
ADDED
package/lib/sonarqube.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const { logInfo, logSuccess } = require('./logger');
|
|
5
|
+
const { installDevDependency } = require('./packageManager');
|
|
6
|
+
|
|
7
|
+
const SONAR_PROPS_FILE = 'sonar-project.properties';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------
|
|
10
|
+
// SonarQube server configuration
|
|
11
|
+
// Currently pointing to local Docker instance for testing
|
|
12
|
+
// Switch to GCP later by changing SONAR_HOST_URL and SONAR_TOKEN
|
|
13
|
+
// ---------------------------------------------------------------
|
|
14
|
+
const SONAR_HOST_URL = 'http://192.168.1.72:9000';
|
|
15
|
+
const SONAR_TOKEN = 'sqa_57476a8e9fe67dcdddfbe5a146a681c9373a3ab8';
|
|
16
|
+
// const SONAR_ORG = 'arjunlatiwala'; // only needed for SonarCloud
|
|
17
|
+
|
|
18
|
+
// Auto-create project on SonarQube via API so developer never needs to do it manually
|
|
19
|
+
async function ensureProjectExists(projectKey, projectName) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const auth = Buffer.from(`${SONAR_TOKEN}:`).toString('base64');
|
|
22
|
+
const postData = `name=${encodeURIComponent(projectName)}&project=${encodeURIComponent(projectKey)}`;
|
|
23
|
+
|
|
24
|
+
const options = {
|
|
25
|
+
hostname: '192.168.1.72',
|
|
26
|
+
port: 9000,
|
|
27
|
+
path: '/api/projects/create',
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Authorization': `Basic ${auth}`,
|
|
31
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
32
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const req = http.request(options, (res) => {
|
|
37
|
+
let data = '';
|
|
38
|
+
res.on('data', chunk => data += chunk);
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
if (res.statusCode === 200 || res.statusCode === 201) {
|
|
41
|
+
logSuccess(`SonarQube project "${projectKey}" created automatically.`);
|
|
42
|
+
} else if (res.statusCode === 400 && data.includes('already exists')) {
|
|
43
|
+
logInfo(`SonarQube project "${projectKey}" already exists.`);
|
|
44
|
+
} else {
|
|
45
|
+
logInfo(`SonarQube project setup: ${res.statusCode} — continuing anyway.`);
|
|
46
|
+
}
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
req.on('error', (err) => {
|
|
52
|
+
logInfo(`SonarQube server unreachable — skipping project creation. (${err.message})`);
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
req.write(postData);
|
|
57
|
+
req.end();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
exports.installSonarScanner = async () => {
|
|
62
|
+
logInfo('Installing sonarqube-scanner as a dev dependency...');
|
|
63
|
+
await installDevDependency('sonarqube-scanner');
|
|
64
|
+
logSuccess('sonarqube-scanner installed.');
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
exports.setupSonarProperties = async () => {
|
|
68
|
+
const propsPath = path.join(process.cwd(), SONAR_PROPS_FILE);
|
|
69
|
+
|
|
70
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
71
|
+
let projectKey = 'my-project';
|
|
72
|
+
let projectName = 'My Project';
|
|
73
|
+
|
|
74
|
+
if (await fs.pathExists(pkgPath)) {
|
|
75
|
+
const pkg = await fs.readJSON(pkgPath);
|
|
76
|
+
if (pkg.name) {
|
|
77
|
+
projectKey = pkg.name.replace(/[^a-zA-Z0-9_\-.:]/g, '_');
|
|
78
|
+
projectName = pkg.name;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Auto-create project so developer never needs to do it manually
|
|
83
|
+
logInfo(`Setting up SonarQube project "${projectKey}"...`);
|
|
84
|
+
await ensureProjectExists(projectKey, projectName);
|
|
85
|
+
|
|
86
|
+
const content = `# ---------------------------------------------------------------
|
|
87
|
+
# SonarQube configuration — auto-generated by secure-husky-setup
|
|
88
|
+
# Do not edit manually
|
|
89
|
+
# ---------------------------------------------------------------
|
|
90
|
+
sonar.host.url=${SONAR_HOST_URL}
|
|
91
|
+
sonar.login=${SONAR_TOKEN}
|
|
92
|
+
sonar.projectKey=${projectKey}
|
|
93
|
+
sonar.projectName=${projectName}
|
|
94
|
+
sonar.projectVersion=1.0
|
|
95
|
+
sonar.sources=.
|
|
96
|
+
sonar.sourceEncoding=UTF-8
|
|
97
|
+
sonar.exclusions=node_modules/**,dist/**,build/**,coverage/**,.husky/**,.tools/**
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
await fs.writeFile(propsPath, content);
|
|
101
|
+
logSuccess(`${SONAR_PROPS_FILE} configured automatically.`);
|
|
102
|
+
};
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
|
|
3
|
+
exports.fileExists = async (path) => {
|
|
4
|
+
return await fs.pathExists(path);
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
exports.readJSON = async (path) => {
|
|
8
|
+
return await fs.readJSON(path);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
exports.writeJSON = async (path, data) => {
|
|
12
|
+
return await fs.writeJSON(path, data, { spaces: 2 });
|
|
13
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "secure-husky-setup",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Automatic Husky + Gitleaks setup for any JS project",
|
|
5
|
+
"main": "bin/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"secure-husky-setup": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"husky",
|
|
11
|
+
"gitleaks",
|
|
12
|
+
"git-hooks",
|
|
13
|
+
"security"
|
|
14
|
+
],
|
|
15
|
+
"author": "Your Name",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"files": ["bin", "lib", "templates"],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"chalk": "^4.1.2",
|
|
20
|
+
"execa": "^5.1.1",
|
|
21
|
+
"fs-extra": "^11.3.3",
|
|
22
|
+
"sonarqube-scanner": "^4.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
name: CI - Newman & Smoke Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- '**'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
# ─────────────────────────────────────────
|
|
10
|
+
# JOB 1: Node.js Smoke Tests
|
|
11
|
+
# ─────────────────────────────────────────
|
|
12
|
+
smoke-tests:
|
|
13
|
+
name: 🔥 Smoke Tests
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Node.js
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: '20'
|
|
24
|
+
cache: 'npm'
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: Build project (if applicable)
|
|
30
|
+
run: |
|
|
31
|
+
if [ "$(node -e "const p=require('./package.json'); console.log(!!p.scripts?.build)")" = "true" ]; then
|
|
32
|
+
npm run build
|
|
33
|
+
else
|
|
34
|
+
echo "No build script found, skipping."
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
- name: Start server in background
|
|
38
|
+
run: |
|
|
39
|
+
npm start &
|
|
40
|
+
echo "SERVER_PID=$!" >> $GITHUB_ENV
|
|
41
|
+
for i in $(seq 1 30); do
|
|
42
|
+
if curl -sf http://localhost:${PORT:-3000}/health > /dev/null 2>&1 || \
|
|
43
|
+
curl -sf http://localhost:${PORT:-3000} > /dev/null 2>&1; then
|
|
44
|
+
echo "✅ Server is up!"
|
|
45
|
+
break
|
|
46
|
+
fi
|
|
47
|
+
echo "Waiting for server... ($i/30)"
|
|
48
|
+
sleep 1
|
|
49
|
+
done
|
|
50
|
+
env:
|
|
51
|
+
NODE_ENV: test
|
|
52
|
+
PORT: ${{ vars.PORT || 3000 }}
|
|
53
|
+
|
|
54
|
+
- name: Run smoke tests
|
|
55
|
+
run: npm test
|
|
56
|
+
env:
|
|
57
|
+
NODE_ENV: test
|
|
58
|
+
PORT: ${{ vars.PORT || 3000 }}
|
|
59
|
+
|
|
60
|
+
- name: Stop server
|
|
61
|
+
if: always()
|
|
62
|
+
run: |
|
|
63
|
+
if [ -n "$SERVER_PID" ]; then
|
|
64
|
+
kill $SERVER_PID || true
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# ─────────────────────────────────────────
|
|
68
|
+
# JOB 2: Newman (Postman Collection) Tests
|
|
69
|
+
# ─────────────────────────────────────────
|
|
70
|
+
newman-tests:
|
|
71
|
+
name: 📬 Newman API Tests
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
needs: smoke-tests
|
|
74
|
+
|
|
75
|
+
steps:
|
|
76
|
+
- name: Checkout code
|
|
77
|
+
uses: actions/checkout@v4
|
|
78
|
+
|
|
79
|
+
- name: Set up Node.js
|
|
80
|
+
uses: actions/setup-node@v4
|
|
81
|
+
with:
|
|
82
|
+
node-version: '20'
|
|
83
|
+
cache: 'npm'
|
|
84
|
+
|
|
85
|
+
- name: Install dependencies
|
|
86
|
+
run: npm ci
|
|
87
|
+
|
|
88
|
+
- name: Install Newman & reporters
|
|
89
|
+
run: npm install -g newman newman-reporter-htmlextra
|
|
90
|
+
|
|
91
|
+
- name: Start server in background
|
|
92
|
+
run: |
|
|
93
|
+
npm start &
|
|
94
|
+
echo "SERVER_PID=$!" >> $GITHUB_ENV
|
|
95
|
+
for i in $(seq 1 30); do
|
|
96
|
+
if curl -sf http://localhost:${PORT:-3000}/health > /dev/null 2>&1 || \
|
|
97
|
+
curl -sf http://localhost:${PORT:-3000} > /dev/null 2>&1; then
|
|
98
|
+
echo "✅ Server is up!"
|
|
99
|
+
break
|
|
100
|
+
fi
|
|
101
|
+
echo "Waiting for server... ($i/30)"
|
|
102
|
+
sleep 1
|
|
103
|
+
done
|
|
104
|
+
env:
|
|
105
|
+
NODE_ENV: test
|
|
106
|
+
PORT: ${{ vars.PORT || 3000 }}
|
|
107
|
+
|
|
108
|
+
- name: Find and run Postman collections
|
|
109
|
+
run: |
|
|
110
|
+
mkdir -p newman-reports
|
|
111
|
+
|
|
112
|
+
COLLECTIONS=$(find . \
|
|
113
|
+
-not -path '*/node_modules/*' \
|
|
114
|
+
-not -path '*/.git/*' \
|
|
115
|
+
\( -name "*.postman_collection.json" -o -name "collection.json" \) \
|
|
116
|
+
2>/dev/null)
|
|
117
|
+
|
|
118
|
+
if [ -z "$COLLECTIONS" ]; then
|
|
119
|
+
echo "⚠️ No Postman collection files found!"
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
ENV_FILE=$(find . \
|
|
124
|
+
-not -path '*/node_modules/*' \
|
|
125
|
+
-not -path '*/.git/*' \
|
|
126
|
+
-name "*.postman_environment.json" \
|
|
127
|
+
2>/dev/null | head -1)
|
|
128
|
+
|
|
129
|
+
for COLLECTION in $COLLECTIONS; do
|
|
130
|
+
REPORT_NAME=$(basename "$COLLECTION" .json)
|
|
131
|
+
echo "▶️ Running collection: $COLLECTION"
|
|
132
|
+
|
|
133
|
+
ENV_FLAG=""
|
|
134
|
+
if [ -n "$ENV_FILE" ]; then
|
|
135
|
+
ENV_FLAG="--environment $ENV_FILE"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
newman run "$COLLECTION" \
|
|
139
|
+
$ENV_FLAG \
|
|
140
|
+
--env-var "baseUrl=http://localhost:${PORT:-3000}" \
|
|
141
|
+
--reporters cli,htmlextra \
|
|
142
|
+
--reporter-htmlextra-export "newman-reports/${REPORT_NAME}-report.html" \
|
|
143
|
+
--reporter-htmlextra-title "${REPORT_NAME} Test Report" \
|
|
144
|
+
--bail
|
|
145
|
+
done
|
|
146
|
+
env:
|
|
147
|
+
NODE_ENV: test
|
|
148
|
+
PORT: ${{ vars.PORT || 3000 }}
|
|
149
|
+
|
|
150
|
+
- name: Upload Newman HTML reports
|
|
151
|
+
if: always()
|
|
152
|
+
uses: actions/upload-artifact@v4
|
|
153
|
+
with:
|
|
154
|
+
name: newman-test-reports
|
|
155
|
+
path: newman-reports/
|
|
156
|
+
retention-days: 14
|
|
157
|
+
|
|
158
|
+
- name: Stop server
|
|
159
|
+
if: always()
|
|
160
|
+
run: |
|
|
161
|
+
if [ -n "$SERVER_PID" ]; then
|
|
162
|
+
kill $SERVER_PID || true
|
|
163
|
+
fi
|