create-qa-architect 5.11.2 → 5.12.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.
@@ -0,0 +1,39 @@
1
+ name: Auto Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+
19
+ - name: Get previous tag
20
+ id: prev_tag
21
+ run: |
22
+ PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
23
+ echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT
24
+
25
+ - name: Generate release notes
26
+ run: |
27
+ TAG=${GITHUB_REF#refs/tags/}
28
+ PREV_TAG=${{ steps.prev_tag.outputs.tag }}
29
+ if [ -n "$PREV_TAG" ]; then
30
+ echo "## Changes since $PREV_TAG" > notes.md
31
+ git log ${PREV_TAG}..${TAG} --pretty=format:"- %s" >> notes.md
32
+ echo -e "\n\n**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${TAG}" >> notes.md
33
+ else
34
+ echo "Initial release" > notes.md
35
+ fi
36
+
37
+ - uses: softprops/action-gh-release@v2
38
+ with:
39
+ body_path: notes.md
@@ -341,7 +341,7 @@ jobs:
341
341
  gitleaks-8.28.0-linux-x64-
342
342
 
343
343
  - name: Run real gitleaks binary verification test
344
- if: runner.os == 'Linux'
344
+ if: runner.os == 'Linux' && hashFiles('tests/gitleaks-real-binary-test.js') != ''
345
345
  run: |
346
346
  echo "🔐 Running real gitleaks binary verification test..."
347
347
  QAA_DEVELOPER=true RUN_REAL_BINARY_TEST=1 node tests/gitleaks-real-binary-test.js
@@ -75,6 +75,9 @@ const baseDevDependencies = {
75
75
  '@lhci/cli': '^0.14.0',
76
76
  vitest: '^2.1.8',
77
77
  '@vitest/coverage-v8': '^2.1.8',
78
+ commitlint: '^20.4.1',
79
+ '@commitlint/cli': '^20.4.1',
80
+ '@commitlint/config-conventional': '^20.4.1',
78
81
  }
79
82
 
80
83
  const typeScriptDevDependencies = {
@@ -91,6 +91,81 @@
91
91
  },
92
92
  "additionalProperties": false
93
93
  }
94
+ },
95
+ "performance": {
96
+ "type": "object",
97
+ "description": "Performance budget configuration",
98
+ "properties": {
99
+ "lighthouse": {
100
+ "type": "object",
101
+ "description": "Lighthouse CI performance thresholds",
102
+ "properties": {
103
+ "performance": {
104
+ "type": "number",
105
+ "minimum": 0,
106
+ "maximum": 1,
107
+ "description": "Minimum performance score (0-1)"
108
+ },
109
+ "accessibility": {
110
+ "type": "number",
111
+ "minimum": 0,
112
+ "maximum": 1,
113
+ "description": "Minimum accessibility score (0-1)"
114
+ },
115
+ "bestPractices": {
116
+ "type": "number",
117
+ "minimum": 0,
118
+ "maximum": 1,
119
+ "description": "Minimum best practices score (0-1)"
120
+ },
121
+ "seo": {
122
+ "type": "number",
123
+ "minimum": 0,
124
+ "maximum": 1,
125
+ "description": "Minimum SEO score (0-1)"
126
+ },
127
+ "maxFCP": {
128
+ "type": "number",
129
+ "minimum": 0,
130
+ "description": "Max First Contentful Paint in ms"
131
+ },
132
+ "maxLCP": {
133
+ "type": "number",
134
+ "minimum": 0,
135
+ "description": "Max Largest Contentful Paint in ms"
136
+ },
137
+ "maxCLS": {
138
+ "type": "number",
139
+ "minimum": 0,
140
+ "description": "Max Cumulative Layout Shift"
141
+ },
142
+ "maxTBT": {
143
+ "type": "number",
144
+ "minimum": 0,
145
+ "description": "Max Total Blocking Time in ms"
146
+ }
147
+ },
148
+ "additionalProperties": false
149
+ },
150
+ "bundleSize": {
151
+ "type": "object",
152
+ "description": "Bundle size limits",
153
+ "properties": {
154
+ "maxJs": {
155
+ "type": "string",
156
+ "pattern": "^[0-9]+ [kKmM][bB]$",
157
+ "description": "Max JS bundle size (e.g., '250 kB')"
158
+ },
159
+ "maxCss": {
160
+ "type": "string",
161
+ "pattern": "^[0-9]+ [kKmM][bB]$",
162
+ "description": "Max CSS bundle size (e.g., '50 kB')"
163
+ }
164
+ },
165
+ "additionalProperties": false
166
+ }
167
+ },
168
+ "additionalProperties": false
94
169
  }
95
170
  },
96
171
  "additionalProperties": false
@@ -119,6 +119,11 @@ class ProjectMaturityDetector {
119
119
  stats.testFiles === 0
120
120
  ) {
121
121
  maturity = 'bootstrap'
122
+ } else if (
123
+ stats.totalSourceFiles < MATURITY_THRESHOLDS.MIN_BOOTSTRAP_FILES &&
124
+ stats.testFiles > 0
125
+ ) {
126
+ maturity = 'bootstrap'
122
127
  } else if (
123
128
  stats.testFiles > 0 &&
124
129
  stats.totalSourceFiles >= MATURITY_THRESHOLDS.MIN_BOOTSTRAP_FILES &&
@@ -25,6 +25,7 @@ function generateLighthouseConfig(options = {}) {
25
25
  hasThresholds = false,
26
26
  collectUrl = 'http://localhost:3000',
27
27
  staticDistDir = null,
28
+ budgets = null,
28
29
  } = options
29
30
 
30
31
  const collectConfig = staticDistDir
@@ -34,29 +35,42 @@ function generateLighthouseConfig(options = {}) {
34
35
  startServerReadyPattern: 'ready|listening|started',
35
36
  startServerReadyTimeout: 30000,`
36
37
 
37
- const assertConfig = hasThresholds
38
- ? `
38
+ let assertConfig
39
+ if (hasThresholds) {
40
+ // Use custom budgets from .qualityrc.json if provided, otherwise defaults
41
+ const b = budgets || {}
42
+ const fcp = b.maxFCP || 2000
43
+ const lcp = b.maxLCP || 2500
44
+ const cls = b.maxCLS || 0.1
45
+ const tbt = b.maxTBT || 300
46
+ const perf = b.performance || 0.8
47
+ const a11y = b.accessibility || 0.9
48
+ const bp = b.bestPractices || 0.9
49
+ const seo = b.seo || 0.9
50
+
51
+ assertConfig = `
39
52
  assert: {
40
53
  preset: 'lighthouse:recommended',
41
54
  assertions: {
42
55
  // Performance
43
- 'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
44
- 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
45
- 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
46
- 'total-blocking-time': ['warn', { maxNumericValue: 300 }],
56
+ 'first-contentful-paint': ['warn', { maxNumericValue: ${fcp} }],
57
+ 'largest-contentful-paint': ['error', { maxNumericValue: ${lcp} }],
58
+ 'cumulative-layout-shift': ['error', { maxNumericValue: ${cls} }],
59
+ 'total-blocking-time': ['warn', { maxNumericValue: ${tbt} }],
47
60
 
48
61
  // Categories (0-1 scale)
49
- 'categories:performance': ['warn', { minScore: 0.8 }],
50
- 'categories:accessibility': ['error', { minScore: 0.9 }],
51
- 'categories:best-practices': ['warn', { minScore: 0.9 }],
52
- 'categories:seo': ['warn', { minScore: 0.9 }],
62
+ 'categories:performance': ['warn', { minScore: ${perf} }],
63
+ 'categories:accessibility': ['error', { minScore: ${a11y} }],
64
+ 'categories:best-practices': ['warn', { minScore: ${bp} }],
65
+ 'categories:seo': ['warn', { minScore: ${seo} }],
53
66
 
54
67
  // Allow some common warnings
55
68
  'unsized-images': 'off',
56
69
  'uses-responsive-images': 'off',
57
70
  },
58
71
  },`
59
- : `
72
+ } else {
73
+ assertConfig = `
60
74
  assert: {
61
75
  // Basic assertions for Free tier - just warnings, no failures
62
76
  assertions: {
@@ -64,6 +78,7 @@ function generateLighthouseConfig(options = {}) {
64
78
  'categories:best-practices': ['warn', { minScore: 0.7 }],
65
79
  },
66
80
  },`
81
+ }
67
82
 
68
83
  return `module.exports = {
69
84
  ci: {
@@ -86,7 +101,7 @@ function generateLighthouseConfig(options = {}) {
86
101
  * @returns {Array} size-limit config array
87
102
  */
88
103
  function generateSizeLimitConfig(options = {}) {
89
- const { projectPath = process.cwd() } = options
104
+ const { projectPath = process.cwd(), budgets = null } = options
90
105
 
91
106
  // Detect build output paths
92
107
  const possibleDists = ['dist', 'build', '.next', 'out', 'public']
@@ -99,6 +114,10 @@ function generateSizeLimitConfig(options = {}) {
99
114
  }
100
115
  }
101
116
 
117
+ // Use custom budgets from .qualityrc.json if provided
118
+ const jsLimit = (budgets && budgets.maxJs) || null
119
+ const cssLimit = (budgets && budgets.maxCss) || null
120
+
102
121
  // Detect if it's a Next.js app
103
122
  const isNextJS =
104
123
  fs.existsSync(path.join(projectPath, 'next.config.js')) ||
@@ -108,7 +127,7 @@ function generateSizeLimitConfig(options = {}) {
108
127
  return [
109
128
  {
110
129
  path: '.next/static/**/*.js',
111
- limit: '300 kB',
130
+ limit: jsLimit || '300 kB',
112
131
  webpack: false,
113
132
  },
114
133
  ]
@@ -117,11 +136,11 @@ function generateSizeLimitConfig(options = {}) {
117
136
  return [
118
137
  {
119
138
  path: `${distDir}/**/*.js`,
120
- limit: '250 kB',
139
+ limit: jsLimit || '250 kB',
121
140
  },
122
141
  {
123
142
  path: `${distDir}/**/*.css`,
124
- limit: '50 kB',
143
+ limit: cssLimit || '50 kB',
125
144
  },
126
145
  ]
127
146
  }
@@ -525,8 +544,8 @@ function getQualityToolsDependencies(features = {}) {
525
544
  }
526
545
 
527
546
  if (features.commitlint) {
528
- deps['@commitlint/cli'] = '^19.0.0'
529
- deps['@commitlint/config-conventional'] = '^19.0.0'
547
+ deps['@commitlint/cli'] = '^20.4.1'
548
+ deps['@commitlint/config-conventional'] = '^20.4.1'
530
549
  }
531
550
 
532
551
  if (features.axeCore) {
@@ -109,46 +109,17 @@ id = "jwt-token"
109
109
  regex = '''eyJ[A-Za-z0-9_/+-]{10,}={0,2}'''
110
110
  tags = ["key", "JWT"]
111
111
 
112
- [[rules]]
113
- description = "Base64 encoded secrets (long)"
114
- id = "base64-secret"
115
- regex = '''[A-Za-z0-9+/]{40,}={0,2}'''
116
- tags = ["secret", "base64"]
117
- keywords = ["secret", "key", "token", "password"]
118
-
119
112
  [[rules]]
120
113
  description = "Environment variable secrets"
121
114
  id = "env-secret"
122
115
  regex = '''(?i)(api_key|secret|password|token)\\s*=\\s*['""][^'"\\s]{10,}['""]'''
123
116
  tags = ["env", "secret"]
124
117
 
125
- # Allowlist for test files and examples
126
- [[rules.allowlist]]
127
- description = "Test secrets and examples"
128
- regexes = [
129
- '''test_secret_.*''',
130
- '''example_.*''',
131
- '''dummy_.*''',
132
- '''fake_.*'''
133
- ]
134
- paths = [
135
- '''tests/''',
136
- '''test/''',
137
- '''__tests__/''',
138
- '''examples/''',
139
- '''docs/''',
140
- '''.md$'''
141
- ]
142
-
143
- # Global allowlist for common false positives
144
- [[allowlist]]
145
- description = "Common false positives"
146
- regexes = [
147
- '''EXAMPLE_.*''',
148
- '''your_.*_here''',
149
- '''replace_with_.*''',
150
- '''TODO:.*'''
151
- ]
118
+ # Allowlist for test/example files (broad exclusion)
119
+ [allowlist]
120
+ description = "Test and example files"
121
+ regexes = ['''test_secret_.*|example_.*|dummy_.*|fake_.*|EXAMPLE_.*|your_.*_here|replace_with_.*|TODO:.*''']
122
+ paths = ['''(tests/|test/|__tests__/|examples/|docs/|\\.md$|\\.gitleaksignore$|node_modules/|dist/|build/|\\.next/|coverage/)''']
152
123
  `
153
124
  }
154
125
 
@@ -179,7 +179,18 @@ function injectWorkflowMode(workflowContent, mode) {
179
179
  "has-css: 'false'"
180
180
  )
181
181
 
182
- // 4. Simplify "Display Detection Report" to show only package manager info
182
+ // 4. Remove dev-only gitleaks binary test steps (only relevant for qa-architect itself)
183
+ // The restore-keys block has 3 lines after `|`, match all of them to avoid stray lines
184
+ updated = updated.replace(
185
+ /\s+- name: Cache gitleaks binary for real download test[\s\S]*?restore-keys: \|[^\n]*\n[^\n]*\n[^\n]*\n/,
186
+ '\n'
187
+ )
188
+ updated = updated.replace(
189
+ /\s+- name: Run real gitleaks binary verification test[\s\S]*?node tests\/gitleaks-real-binary-test\.js\n/,
190
+ '\n'
191
+ )
192
+
193
+ // 5. Simplify "Display Detection Report" to show only package manager info
183
194
  updated = updated.replace(
184
195
  /- name: Display Detection Report\s+run: \|[\s\S]*?echo "Has CSS files:[^"]*"/,
185
196
  `- name: Display Detection Report
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-qa-architect",
3
- "version": "5.11.2",
3
+ "version": "5.12.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": {
@@ -51,7 +51,8 @@
51
51
  "size": "size-limit",
52
52
  "size:why": "size-limit --why",
53
53
  "test:a11y": "vitest run tests/accessibility.test.js",
54
- "test:coverage:check": "vitest run --coverage --coverage.thresholds.lines=70 --coverage.thresholds.functions=70"
54
+ "test:coverage:check": "vitest run --coverage --coverage.thresholds.lines=70 --coverage.thresholds.functions=70",
55
+ "test:changed": "vitest run --changed HEAD~1 --passWithNoTests"
55
56
  },
56
57
  "keywords": [
57
58
  "qa-architect",
@@ -82,11 +82,17 @@ echo " ⚡ Speed Bonus: $SPEED_BONUS"
82
82
  echo ""
83
83
 
84
84
  # Test tier selection based on risk score
85
+ # NOTE: E2E tests and slow command tests are ALWAYS excluded from pre-push
86
+ # - E2E tests: Require dev server, browsers, proper infrastructure (run in CI only)
87
+ # - Command tests: Take 60+ seconds, verify npm scripts work (run in CI only)
88
+ # These run in GitHub Actions on every PR and push to main
89
+
85
90
  if [[ $RISK_SCORE -ge 7 ]]; then
86
- echo "🔴 HIGH RISK - Comprehensive validation"
87
- echo " • All tests + security audit"
88
- # Runs: npm run test:comprehensive 2>/dev/null || npm run test 2>/dev/null || npm test
89
- npm run test:comprehensive 2>/dev/null || npm run test 2>/dev/null || npm test
91
+ echo "🔴 HIGH RISK - Comprehensive validation (pre-push)"
92
+ echo " • Unit + integration tests + security audit"
93
+ echo " • (E2E and command tests run in CI only)"
94
+ # Runs: npm run test:medium 2>/dev/null || npm run test:fast 2>/dev/null || npm test
95
+ npm run test:medium 2>/dev/null || npm run test:fast 2>/dev/null || npm test
90
96
  elif [[ $RISK_SCORE -ge 4 ]]; then
91
97
  echo "🟡 MEDIUM RISK - Standard validation"
92
98
  echo " • Fast tests + integration (excludes slow tests)"
package/setup.js CHANGED
@@ -927,13 +927,44 @@ HELP:
927
927
  const hasConventionalCommits = hasFeature('conventionalCommits')
928
928
  const hasCoverageThresholds = hasFeature('coverageThresholds')
929
929
 
930
+ // Load .qualityrc.json for performance budgets and check overrides
931
+ let performanceBudgets = null
932
+ let qualityChecks = {}
933
+ const qualityrcPath = path.join(projectPath, '.qualityrc.json')
934
+ if (fs.existsSync(qualityrcPath)) {
935
+ try {
936
+ const qualityrc = JSON.parse(fs.readFileSync(qualityrcPath, 'utf8'))
937
+ if (qualityrc.performance) {
938
+ performanceBudgets = qualityrc.performance
939
+ }
940
+ if (qualityrc.checks) {
941
+ qualityChecks = qualityrc.checks
942
+ }
943
+ } catch {
944
+ // Ignore parse errors - config validation handles this elsewhere
945
+ }
946
+ }
947
+
948
+ // Helper: check if a quality check is enabled (respects .qualityrc.json overrides)
949
+ function isCheckEnabled(checkName, licenseDefault) {
950
+ const override = qualityChecks[checkName]
951
+ if (override && override.enabled === false) return false
952
+ if (override && override.enabled === true) return true
953
+ // "auto" or missing: use license-based default
954
+ return licenseDefault
955
+ }
956
+
930
957
  // 1. Lighthouse CI - available to all, thresholds for Pro+
931
- if (hasLighthouse) {
958
+ if (isCheckEnabled('lighthouse', hasLighthouse)) {
932
959
  try {
933
960
  const lighthousePath = path.join(projectPath, 'lighthouserc.js')
934
961
  if (!fs.existsSync(lighthousePath)) {
935
962
  writeLighthouseConfig(projectPath, {
936
963
  hasThresholds: hasLighthouseThresholds,
964
+ budgets:
965
+ performanceBudgets && performanceBudgets.lighthouse
966
+ ? performanceBudgets.lighthouse
967
+ : null,
937
968
  })
938
969
  addedTools.push(
939
970
  hasLighthouseThresholds
@@ -953,7 +984,12 @@ HELP:
953
984
  if (hasBundleSizeLimits) {
954
985
  try {
955
986
  if (!pkgJson.content['size-limit']) {
956
- writeSizeLimitConfig(projectPath)
987
+ writeSizeLimitConfig(projectPath, {
988
+ budgets:
989
+ performanceBudgets && performanceBudgets.bundleSize
990
+ ? performanceBudgets.bundleSize
991
+ : null,
992
+ })
957
993
  addedTools.push('Bundle size limits (size-limit)')
958
994
  }
959
995
  } catch (error) {