create-qa-architect 5.0.6 → 5.3.1
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/.github/workflows/auto-release.yml +49 -0
- package/.github/workflows/dependabot-auto-merge.yml +32 -0
- package/LICENSE +3 -3
- package/README.md +54 -15
- package/docs/ADOPTION-SUMMARY.md +41 -0
- package/docs/ARCHITECTURE-REVIEW.md +67 -0
- package/docs/ARCHITECTURE.md +29 -41
- package/docs/CODE-REVIEW.md +100 -0
- package/docs/PREFLIGHT_REPORT.md +32 -40
- package/docs/REQUIREMENTS.md +148 -0
- package/docs/SECURITY-AUDIT.md +68 -0
- package/docs/TESTING.md +3 -4
- package/docs/test-trace-matrix.md +28 -0
- package/lib/billing-dashboard.html +6 -12
- package/lib/commands/deps.js +245 -0
- package/lib/commands/index.js +25 -0
- package/lib/commands/validate.js +85 -0
- package/lib/error-reporter.js +13 -1
- package/lib/github-api.js +108 -13
- package/lib/license-signing.js +110 -0
- package/lib/license-validator.js +359 -71
- package/lib/licensing.js +343 -111
- package/lib/prelaunch-validator.js +828 -0
- package/lib/quality-tools-generator.js +495 -0
- package/lib/result-types.js +112 -0
- package/lib/security-enhancements.js +1 -1
- package/lib/smart-strategy-generator.js +28 -9
- package/lib/template-loader.js +52 -19
- package/lib/validation/cache-manager.js +36 -6
- package/lib/validation/config-security.js +82 -15
- package/lib/validation/workflow-validation.js +49 -23
- package/package.json +8 -10
- package/scripts/check-test-coverage.sh +46 -0
- package/setup.js +356 -285
- package/templates/QUALITY_TROUBLESHOOTING.md +32 -33
- package/templates/scripts/smart-test-strategy.sh +1 -1
- package/create-saas-monetization.js +0 -1513
package/lib/template-loader.js
CHANGED
|
@@ -34,7 +34,11 @@ class TemplateLoader {
|
|
|
34
34
|
try {
|
|
35
35
|
const stats = fs.statSync(templatePath)
|
|
36
36
|
return stats.isDirectory()
|
|
37
|
-
} catch {
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Silent failure fix: Log unexpected errors in debug mode
|
|
39
|
+
if (process.env.DEBUG && error.code !== 'ENOENT') {
|
|
40
|
+
console.warn(`⚠️ Template path check failed: ${error.message}`)
|
|
41
|
+
}
|
|
38
42
|
return false
|
|
39
43
|
}
|
|
40
44
|
}
|
|
@@ -51,7 +55,13 @@ class TemplateLoader {
|
|
|
51
55
|
|
|
52
56
|
const stats = fs.statSync(templateDir)
|
|
53
57
|
return stats.isDirectory()
|
|
54
|
-
} catch {
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Silent failure fix: Log unexpected errors in debug mode
|
|
60
|
+
if (process.env.DEBUG && error.code !== 'ENOENT') {
|
|
61
|
+
console.warn(
|
|
62
|
+
`⚠️ Template structure validation failed: ${error.message}`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
55
65
|
return false
|
|
56
66
|
}
|
|
57
67
|
}
|
|
@@ -128,6 +138,7 @@ class TemplateLoader {
|
|
|
128
138
|
async loadTemplates(dir, baseDir = dir, isPackageDir = false) {
|
|
129
139
|
/** @type {Record<string, string>} */
|
|
130
140
|
const templates = {}
|
|
141
|
+
const errors = []
|
|
131
142
|
|
|
132
143
|
try {
|
|
133
144
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
@@ -144,29 +155,51 @@ class TemplateLoader {
|
|
|
144
155
|
const fullPath = path.join(dir, entry.name)
|
|
145
156
|
const relativePath = path.relative(baseDir, fullPath)
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
try {
|
|
159
|
+
if (entry.isDirectory()) {
|
|
160
|
+
// Recursively load from subdirectories
|
|
161
|
+
const subTemplates = await this.loadTemplates(
|
|
162
|
+
fullPath,
|
|
163
|
+
baseDir,
|
|
164
|
+
false
|
|
165
|
+
)
|
|
166
|
+
Object.assign(templates, subTemplates)
|
|
167
|
+
} else if (entry.isFile()) {
|
|
168
|
+
// DR6 fix: Handle individual file errors separately
|
|
169
|
+
const content = await fs.promises.readFile(fullPath, 'utf8')
|
|
170
|
+
templates[relativePath] = content
|
|
171
|
+
}
|
|
172
|
+
} catch (fileError) {
|
|
173
|
+
// Track individual file errors
|
|
174
|
+
errors.push({ file: relativePath, error: fileError.message })
|
|
175
|
+
|
|
176
|
+
if (this.strict) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Failed to load template ${relativePath}: ${fileError.message}`
|
|
179
|
+
)
|
|
180
|
+
} else if (this.verbose) {
|
|
181
|
+
console.warn(
|
|
182
|
+
`⚠️ Skipping template ${relativePath}: ${fileError.message}`
|
|
183
|
+
)
|
|
184
|
+
}
|
|
160
185
|
}
|
|
161
186
|
}
|
|
162
187
|
} catch (error) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
188
|
+
const message = `Could not load templates from ${dir}: ${error.message}`
|
|
189
|
+
|
|
190
|
+
if (this.strict) {
|
|
191
|
+
throw new Error(message)
|
|
192
|
+
} else if (this.verbose) {
|
|
193
|
+
console.warn(`⚠️ ${message}`)
|
|
167
194
|
}
|
|
168
195
|
}
|
|
169
196
|
|
|
197
|
+
// DR6 fix: Alert if critical templates are missing
|
|
198
|
+
if (errors.length > 0 && this.verbose) {
|
|
199
|
+
console.warn(`⚠️ ${errors.length} template file(s) failed to load`)
|
|
200
|
+
console.warn(' This may result in incomplete configuration')
|
|
201
|
+
}
|
|
202
|
+
|
|
170
203
|
return templates
|
|
171
204
|
}
|
|
172
205
|
|
|
@@ -40,8 +40,14 @@ class CacheManager {
|
|
|
40
40
|
try {
|
|
41
41
|
const content = fs.readFileSync(filePath, 'utf8')
|
|
42
42
|
return crypto.createHash('sha256').update(content).digest('hex')
|
|
43
|
-
} catch {
|
|
43
|
+
} catch (error) {
|
|
44
44
|
// If file doesn't exist or can't be read, return timestamp-based key
|
|
45
|
+
// Silent failure fix: Log unexpected errors in debug mode
|
|
46
|
+
if (process.env.DEBUG && error.code !== 'ENOENT') {
|
|
47
|
+
console.warn(
|
|
48
|
+
`⚠️ Cache key generation failed for ${filePath}: ${error.message}`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
45
51
|
return crypto
|
|
46
52
|
.createHash('sha256')
|
|
47
53
|
.update(`${filePath}-${Date.now()}`)
|
|
@@ -89,8 +95,12 @@ class CacheManager {
|
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
return cached.result
|
|
92
|
-
} catch {
|
|
98
|
+
} catch (error) {
|
|
93
99
|
// If cache file is corrupted or unreadable, treat as cache miss
|
|
100
|
+
// Silent failure fix: Log unexpected errors in debug mode
|
|
101
|
+
if (process.env.DEBUG && error.code !== 'ENOENT') {
|
|
102
|
+
console.warn(`⚠️ Cache read failed: ${error.message}`)
|
|
103
|
+
}
|
|
94
104
|
return null
|
|
95
105
|
}
|
|
96
106
|
}
|
|
@@ -127,24 +137,44 @@ class CacheManager {
|
|
|
127
137
|
*/
|
|
128
138
|
clear() {
|
|
129
139
|
if (!this.enabled) {
|
|
130
|
-
return
|
|
140
|
+
return { success: true, cleared: 0 }
|
|
131
141
|
}
|
|
132
142
|
|
|
143
|
+
const results = { success: true, cleared: 0, errors: [] }
|
|
144
|
+
|
|
133
145
|
try {
|
|
134
146
|
if (fs.existsSync(this.cacheDir)) {
|
|
135
147
|
const files = fs.readdirSync(this.cacheDir)
|
|
136
148
|
files.forEach(file => {
|
|
137
149
|
if (file.endsWith('.json')) {
|
|
138
|
-
|
|
150
|
+
try {
|
|
151
|
+
fs.unlinkSync(path.join(this.cacheDir, file))
|
|
152
|
+
results.cleared++
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// DR11 fix: Track individual file errors
|
|
155
|
+
results.errors.push({ file, error: error.message })
|
|
156
|
+
results.success = false
|
|
157
|
+
}
|
|
139
158
|
}
|
|
140
159
|
})
|
|
141
160
|
}
|
|
142
161
|
} catch (error) {
|
|
143
|
-
//
|
|
162
|
+
// DR11 fix: Report directory-level failures
|
|
163
|
+
results.success = false
|
|
164
|
+
results.errors.push({ directory: this.cacheDir, error: error.message })
|
|
165
|
+
|
|
144
166
|
if (this.verbose) {
|
|
145
|
-
console.warn(
|
|
167
|
+
console.warn(`❌ Cache clear failed: ${error.message}`)
|
|
146
168
|
}
|
|
147
169
|
}
|
|
170
|
+
|
|
171
|
+
// DR11 fix: Warn if any failures occurred
|
|
172
|
+
if (!results.success && this.verbose) {
|
|
173
|
+
console.warn(`⚠️ Failed to clear ${results.errors.length} cache file(s)`)
|
|
174
|
+
console.warn(' Some cached validation results may be stale')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return results
|
|
148
178
|
}
|
|
149
179
|
|
|
150
180
|
/**
|
|
@@ -5,7 +5,7 @@ const path = require('path')
|
|
|
5
5
|
const os = require('os')
|
|
6
6
|
const crypto = require('crypto')
|
|
7
7
|
const https = require('https')
|
|
8
|
-
const { execSync } = require('child_process')
|
|
8
|
+
const { execSync, spawnSync } = require('child_process')
|
|
9
9
|
const { showProgress } = require('../ui-helpers')
|
|
10
10
|
|
|
11
11
|
// Pinned gitleaks version for reproducible security scanning
|
|
@@ -15,6 +15,8 @@ const GITLEAKS_VERSION = '8.28.0'
|
|
|
15
15
|
const GITLEAKS_CHECKSUMS = {
|
|
16
16
|
'linux-x64':
|
|
17
17
|
'5fd1b3b0073269484d40078662e921d07427340ab9e6ed526ccd215a565b3298',
|
|
18
|
+
'linux-arm64':
|
|
19
|
+
'3770c7ebeb625e3e96c183525ca18285a01aedef2d75a2c41ceb3e141af2e8b7',
|
|
18
20
|
'darwin-x64':
|
|
19
21
|
'cf09ad7a85683d90221db8324f036f23c8c29107145e1fc4a0dffbfa9e89c09a',
|
|
20
22
|
'darwin-arm64':
|
|
@@ -200,6 +202,7 @@ class ConfigSecurityScanner {
|
|
|
200
202
|
'darwin-x64': 'darwin_x64',
|
|
201
203
|
'darwin-arm64': 'darwin_arm64',
|
|
202
204
|
'linux-x64': 'linux_x64',
|
|
205
|
+
'linux-arm64': 'linux_arm64',
|
|
203
206
|
'win32-x64': 'windows_x64',
|
|
204
207
|
}
|
|
205
208
|
|
|
@@ -210,20 +213,37 @@ class ConfigSecurityScanner {
|
|
|
210
213
|
/**
|
|
211
214
|
* Download file from URL to target path
|
|
212
215
|
*/
|
|
213
|
-
downloadFile(url, targetPath) {
|
|
214
|
-
|
|
215
|
-
const file = fs.createWriteStream(targetPath)
|
|
216
|
+
downloadFile(url, targetPath, redirectCount = 0) {
|
|
217
|
+
const MAX_REDIRECTS = 5
|
|
216
218
|
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
217
220
|
https
|
|
218
221
|
.get(url, response => {
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
if (
|
|
223
|
+
response.statusCode === 302 ||
|
|
224
|
+
response.statusCode === 301 ||
|
|
225
|
+
response.statusCode === 303 ||
|
|
226
|
+
response.statusCode === 307 ||
|
|
227
|
+
response.statusCode === 308
|
|
228
|
+
) {
|
|
229
|
+
if (redirectCount >= MAX_REDIRECTS) {
|
|
230
|
+
reject(new Error('Too many redirects while downloading gitleaks'))
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
const nextUrl = response.headers.location
|
|
234
|
+
if (!nextUrl) {
|
|
235
|
+
reject(new Error('Redirect without location header'))
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
response.resume()
|
|
239
|
+
this.downloadFile(nextUrl, targetPath, redirectCount + 1)
|
|
222
240
|
.then(resolve)
|
|
223
241
|
.catch(reject)
|
|
242
|
+
return
|
|
224
243
|
}
|
|
225
244
|
|
|
226
245
|
if (response.statusCode !== 200) {
|
|
246
|
+
response.resume()
|
|
227
247
|
reject(
|
|
228
248
|
new Error(
|
|
229
249
|
`HTTP ${response.statusCode}: ${response.statusMessage}`
|
|
@@ -232,6 +252,7 @@ class ConfigSecurityScanner {
|
|
|
232
252
|
return
|
|
233
253
|
}
|
|
234
254
|
|
|
255
|
+
const file = fs.createWriteStream(targetPath)
|
|
235
256
|
response.pipe(file)
|
|
236
257
|
|
|
237
258
|
file.on('finish', () => {
|
|
@@ -348,8 +369,10 @@ class ConfigSecurityScanner {
|
|
|
348
369
|
execSync(auditCmd, {
|
|
349
370
|
stdio: 'pipe',
|
|
350
371
|
timeout: 60000, // 60 second timeout for audit operations
|
|
372
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
351
373
|
encoding: 'utf8',
|
|
352
374
|
})
|
|
375
|
+
spinner.succeed('npm audit completed - no high/critical vulnerabilities')
|
|
353
376
|
} catch (error) {
|
|
354
377
|
if (error.signal === 'SIGTERM') {
|
|
355
378
|
// Timeout occurred
|
|
@@ -364,11 +387,13 @@ class ConfigSecurityScanner {
|
|
|
364
387
|
const auditResult = JSON.parse(error.stdout.toString())
|
|
365
388
|
if (auditResult.metadata && auditResult.metadata.vulnerabilities) {
|
|
366
389
|
const vulns = auditResult.metadata.vulnerabilities
|
|
367
|
-
const total = vulns.high + vulns.critical
|
|
390
|
+
const total = vulns.high + vulns.critical
|
|
368
391
|
if (total > 0) {
|
|
369
|
-
spinner.fail(
|
|
392
|
+
spinner.fail(
|
|
393
|
+
`npm audit found ${total} high/critical vulnerabilities`
|
|
394
|
+
)
|
|
370
395
|
this.issues.push(
|
|
371
|
-
`${packageManager} audit: ${total} vulnerabilities found (${vulns.high} high, ${vulns.critical} critical). Run '${packageManager} audit fix' to resolve.`
|
|
396
|
+
`${packageManager} audit: ${total} high/critical vulnerabilities found (${vulns.high} high, ${vulns.critical} critical). Run '${packageManager} audit fix' to resolve.`
|
|
372
397
|
)
|
|
373
398
|
} else {
|
|
374
399
|
spinner.succeed(
|
|
@@ -401,16 +426,34 @@ class ConfigSecurityScanner {
|
|
|
401
426
|
|
|
402
427
|
// Build command - handle npx vs direct binary execution
|
|
403
428
|
const isNpxCommand = gitleaksBinary.startsWith('npx ')
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
429
|
+
const args = ['detect', '--source', '.', '--redact']
|
|
430
|
+
const command = isNpxCommand ? 'npx' : gitleaksBinary
|
|
431
|
+
const commandArgs = isNpxCommand ? ['gitleaks', ...args] : args
|
|
407
432
|
|
|
408
433
|
// Run gitleaks with --redact to prevent secret exposure and timeout for safety
|
|
409
|
-
|
|
434
|
+
const result = spawnSync(command, commandArgs, {
|
|
410
435
|
stdio: 'pipe',
|
|
411
436
|
timeout: 30000, // 30 second timeout to prevent hangs
|
|
412
437
|
encoding: 'utf8',
|
|
413
438
|
})
|
|
439
|
+
|
|
440
|
+
if (result.error) {
|
|
441
|
+
const error = result.error
|
|
442
|
+
error.stdout = result.stdout
|
|
443
|
+
error.stderr = result.stderr
|
|
444
|
+
error.status = result.status
|
|
445
|
+
error.signal = result.signal
|
|
446
|
+
throw error
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (result.status !== 0) {
|
|
450
|
+
const error = new Error('gitleaks exited with non-zero status')
|
|
451
|
+
error.status = result.status
|
|
452
|
+
error.stdout = result.stdout
|
|
453
|
+
error.stderr = result.stderr
|
|
454
|
+
error.signal = result.signal
|
|
455
|
+
throw error
|
|
456
|
+
}
|
|
414
457
|
spinner.succeed('gitleaks scan completed - no secrets detected')
|
|
415
458
|
} catch (error) {
|
|
416
459
|
if (error.signal === 'SIGTERM') {
|
|
@@ -672,14 +715,38 @@ class ConfigSecurityScanner {
|
|
|
672
715
|
const secretPattern =
|
|
673
716
|
/(?:SECRET|PASSWORD|KEY|TOKEN)\s*=\s*["']?[^"\s']+/gi
|
|
674
717
|
if (secretPattern.test(envStatement)) {
|
|
718
|
+
const redacted = this.redactDockerEnv(envStatement)
|
|
675
719
|
this.issues.push(
|
|
676
|
-
`Dockerfile: Hardcoded secret in ENV statement: ${
|
|
720
|
+
`Dockerfile: Hardcoded secret in ENV statement: ${redacted}`
|
|
677
721
|
)
|
|
678
722
|
}
|
|
679
723
|
}
|
|
680
724
|
}
|
|
681
725
|
}
|
|
682
726
|
|
|
727
|
+
redactDockerEnv(statement) {
|
|
728
|
+
const trimmed = statement.trim()
|
|
729
|
+
const match = trimmed.match(/^ENV\s+(.+)$/i)
|
|
730
|
+
if (!match) return trimmed
|
|
731
|
+
|
|
732
|
+
const body = match[1].trim()
|
|
733
|
+
if (body.includes('=')) {
|
|
734
|
+
const redactedBody = body.replace(
|
|
735
|
+
/=\s*(?:'[^']*'|"[^"]*"|[^ \t\r\n]+)/g,
|
|
736
|
+
'=[REDACTED]'
|
|
737
|
+
)
|
|
738
|
+
return `ENV ${redactedBody}`
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const parts = body.split(/\s+/)
|
|
742
|
+
if (parts.length >= 2) {
|
|
743
|
+
parts[1] = '[REDACTED]'
|
|
744
|
+
return `ENV ${parts.join(' ')}`
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return `ENV ${body}`
|
|
748
|
+
}
|
|
749
|
+
|
|
683
750
|
/**
|
|
684
751
|
* Check .gitignore for security-sensitive files
|
|
685
752
|
*/
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
|
-
const { execSync } = require('child_process')
|
|
6
5
|
const { showProgress } = require('../ui-helpers')
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -85,32 +84,59 @@ class WorkflowValidator {
|
|
|
85
84
|
const spinner = showProgress('Running actionlint on workflow files...')
|
|
86
85
|
|
|
87
86
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
87
|
+
const { createLinter } = require('actionlint')
|
|
88
|
+
const workflowFiles = fs
|
|
89
|
+
.readdirSync(workflowDir)
|
|
90
|
+
.filter(file => file.endsWith('.yml') || file.endsWith('.yaml'))
|
|
91
|
+
|
|
92
|
+
const linter = await createLinter()
|
|
93
|
+
let issueCount = 0
|
|
94
|
+
|
|
95
|
+
for (const file of workflowFiles) {
|
|
96
|
+
const filePath = path.join(workflowDir, file)
|
|
97
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const results = linter(content, filePath) || []
|
|
101
|
+
|
|
102
|
+
if (Array.isArray(results) && results.length > 0) {
|
|
103
|
+
// Filter out false positives for 'vars' context (valid GitHub feature, not recognized by actionlint WASM)
|
|
104
|
+
const filteredResults = results.filter(
|
|
105
|
+
result =>
|
|
106
|
+
!(
|
|
107
|
+
result.kind === 'expression' &&
|
|
108
|
+
result.message?.includes('undefined variable "vars"')
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
issueCount += filteredResults.length
|
|
112
|
+
filteredResults.forEach(result => {
|
|
113
|
+
this.issues.push(
|
|
114
|
+
`actionlint: ${result.file}:${result.line}:${result.column} ${result.kind} - ${result.message}`
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
} catch (lintError) {
|
|
119
|
+
// WASM actionlint has limits on file complexity - skip silently for large files
|
|
120
|
+
if (
|
|
121
|
+
lintError.message?.includes('unreachable') &&
|
|
122
|
+
content.length > 10000
|
|
123
|
+
) {
|
|
124
|
+
// Large file exceeded WASM limits - not a validation failure
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
throw lintError
|
|
110
128
|
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (issueCount > 0) {
|
|
132
|
+
spinner.fail(`actionlint found ${issueCount} issue(s)`)
|
|
111
133
|
} else {
|
|
112
134
|
spinner.succeed('actionlint validation passed')
|
|
113
135
|
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
spinner.fail('actionlint failed to run')
|
|
138
|
+
const reason = error?.message || 'Unknown error'
|
|
139
|
+
this.issues.push(`actionlint: Failed to run - ${reason}`)
|
|
114
140
|
}
|
|
115
141
|
}
|
|
116
142
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-qa-architect",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"description": "QA Architect - Bootstrap quality automation for JavaScript/TypeScript and Python projects with GitHub Actions, pre-commit hooks, linting, formatting, and smart test strategy",
|
|
5
5
|
"main": "setup.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"create-qa-architect": "./setup.js"
|
|
8
|
-
"create-saas-monetization": "./create-saas-monetization.js"
|
|
7
|
+
"create-qa-architect": "./setup.js"
|
|
9
8
|
},
|
|
10
9
|
"scripts": {
|
|
11
10
|
"prepare": "husky",
|
|
@@ -66,7 +65,6 @@
|
|
|
66
65
|
"license": "SEE LICENSE IN LICENSE",
|
|
67
66
|
"files": [
|
|
68
67
|
"setup.js",
|
|
69
|
-
"create-saas-monetization.js",
|
|
70
68
|
"config/",
|
|
71
69
|
"docs/",
|
|
72
70
|
"lib/",
|
|
@@ -91,7 +89,7 @@
|
|
|
91
89
|
"bugs": {
|
|
92
90
|
"url": "https://github.com/vibebuildlab/qa-architect/issues"
|
|
93
91
|
},
|
|
94
|
-
"homepage": "https://vibebuildlab.com/
|
|
92
|
+
"homepage": "https://vibebuildlab.com/qa-architect",
|
|
95
93
|
"engines": {
|
|
96
94
|
"node": ">=20"
|
|
97
95
|
},
|
|
@@ -110,13 +108,13 @@
|
|
|
110
108
|
"actionlint": "^2.0.6",
|
|
111
109
|
"typescript": "^5",
|
|
112
110
|
"c8": "^10.1.2",
|
|
113
|
-
"eslint": "^9.
|
|
111
|
+
"eslint": "^9.39.2",
|
|
114
112
|
"eslint-plugin-security": "^3.0.1",
|
|
115
113
|
"globals": "^15.9.0",
|
|
116
114
|
"husky": "^9.1.4",
|
|
117
115
|
"lint-staged": "^15.2.10",
|
|
118
|
-
"prettier": "^3.
|
|
119
|
-
"stylelint": "^16.
|
|
116
|
+
"prettier": "^3.7.4",
|
|
117
|
+
"stylelint": "^16.26.1",
|
|
120
118
|
"stylelint-config-standard": "^37.0.0"
|
|
121
119
|
},
|
|
122
120
|
"volta": {
|
|
@@ -159,11 +157,11 @@
|
|
|
159
157
|
]
|
|
160
158
|
},
|
|
161
159
|
"dependencies": {
|
|
162
|
-
"@npmcli/package-json": "^7.0.
|
|
160
|
+
"@npmcli/package-json": "^7.0.4",
|
|
163
161
|
"ajv": "^8.17.1",
|
|
164
162
|
"ajv-formats": "^3.0.1",
|
|
165
163
|
"js-yaml": "^4.1.0",
|
|
166
|
-
"markdownlint-cli2": "^0.
|
|
164
|
+
"markdownlint-cli2": "^0.20.0",
|
|
167
165
|
"ora": "^8.1.1",
|
|
168
166
|
"tar": "^7.4.3"
|
|
169
167
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Check test coverage for new features
|
|
3
|
+
# Run manually or via pre-commit hook (warning only)
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
YELLOW='\033[1;33m'
|
|
8
|
+
NC='\033[0m'
|
|
9
|
+
|
|
10
|
+
warnings=0
|
|
11
|
+
|
|
12
|
+
echo "🔍 Checking test coverage..."
|
|
13
|
+
|
|
14
|
+
# Find source files without corresponding test files
|
|
15
|
+
for file in $(find src -name "*.ts" -o -name "*.tsx" 2>/dev/null | grep -v "\.test\." | grep -v "\.d\.ts" || true); do
|
|
16
|
+
# Skip layout, page, and config files (Next.js conventions)
|
|
17
|
+
if [[ "$file" == *"/layout."* ]] || [[ "$file" == *"/page."* ]] || [[ "$file" == *".config."* ]]; then
|
|
18
|
+
continue
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Skip API routes (tested via integration tests)
|
|
22
|
+
if [[ "$file" == *"/api/"* ]]; then
|
|
23
|
+
continue
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Check for corresponding test file
|
|
27
|
+
test_file="${file%.*}.test.${file##*.}"
|
|
28
|
+
|
|
29
|
+
if [ ! -f "$test_file" ] && [ ! -f "tests/unit/$(basename ${file%.*}).test.ts" ]; then
|
|
30
|
+
# Only warn for lib/ and components/ (core business logic)
|
|
31
|
+
if [[ "$file" == *"/lib/"* ]] || [[ "$file" == *"/components/"* ]]; then
|
|
32
|
+
echo -e "${YELLOW}⚠️ May need test: $file${NC}"
|
|
33
|
+
((warnings++)) || true
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
if [ $warnings -gt 0 ]; then
|
|
39
|
+
echo ""
|
|
40
|
+
echo -e "${YELLOW}⚠️ $warnings file(s) in lib/ or components/ may need tests${NC}"
|
|
41
|
+
echo " Run: pnpm test:coverage to check actual coverage"
|
|
42
|
+
else
|
|
43
|
+
echo "✅ All core files appear to have tests"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
exit 0
|