-
Notifications
You must be signed in to change notification settings - Fork 124
4894 lines (4386 loc) · 251 KB
/
Copy pathauto-qa.yml
File metadata and controls
4894 lines (4386 loc) · 251 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Auto-QA Agent
# Quality checks 4x/day with rotating focus areas.
# Layer 1 (always): Build, lint, Go build, bundle size, npm audit
# Layer 2 (always): Resilience, Inventory consistency, UI design principles, ROADMAP/governance
# Layer 3 (always): NFR coverage (testing, i18n, state, navigation, efficiency)
# Layer 4 (always): Flicker detection, code centralization, demo data coverage
# Layer 5 (always): Console error patterns, button/action consistency, stale data
# Layer 6 (8-day rotation via day-of-year % 8):
# 0=Performance, 1=Security, 2=Navigation/A11y,
# 3=Operator Usefulness, 4=SRE/Multi-Cluster,
# 5=Feature Recommendations, 6=Resilience, 7=Consistency
# Layer 7 (weekly): Self-improvement analysis based on recent PR patterns
# Layer 8 (always): Adoption psychology — curiosity, discovery, stickiness
#
# Issues auto-assign Copilot and feed into the automation pipeline.
#
# IMPORTANT: The CONSOLE_AUTO secret (PAT) must have these permissions:
# - repo (full control) - for contents, issues, pull requests
# - workflow - for actions access
# Or use a Fine-Grained PAT with: Contents (read/write), Issues (read/write),
# Pull requests (read/write), Actions (read), Metadata (read)
on:
schedule:
- cron: '17 1,7,13,19 * * *' # 4x/day at 01:17, 07:17, 13:17, 19:17 UTC (saves ~83% PRUs vs hourly)
workflow_dispatch:
inputs:
skip_audit:
description: 'Skip npm audit check'
required: false
default: false
type: boolean
focus_override:
description: 'Override focus (performance|security|a11y|operator|sre|features|resilience|consistency|adoption|none)'
required: false
default: ''
type: string
env:
BUNDLE_SIZE_LIMIT_KB: 5120
MAX_ISSUES_PER_RUN: 3
ASSIGN_COPILOT: 'false' # Set to 'true' to auto-assign Copilot to created issues
COPILOT_ASSIGNMENT_DELAY_S: 120 # Seconds between Copilot assignments to avoid rate limiting
ISSUE_PREFIX: "[Auto-QA]"
NODE_VERSION: "22"
GO_VERSION: "1.26.3"
permissions: read-all
jobs:
auto-qa:
permissions:
contents: read
issues: write
actions: read
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# ── Setup ──────────────────────────────────────────────────────
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: web/package-lock.json
- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
- name: Install frontend dependencies
working-directory: web
run: npm ci
- name: Read tuning config
id: tuning
run: |
CONFIG=".github/auto-qa-tuning.json"
if [ -f "$CONFIG" ]; then
BLOCKED=$(jq -r '[.categories | to_entries[] | select(.value.status == "blocked") | .key] | join(",")' "$CONFIG")
BOOSTED=$(jq -r '[.categories | to_entries[] | select(.value.status == "boosted") | .key] | join(",")' "$CONFIG")
MAX_ISSUES=$(jq -r '.max_issues_override // 0' "$CONFIG")
PR_GUIDANCE=$(jq -r '.pr_guidance // ""' "$CONFIG")
# Export rotation weights as JSON for the focus step
jq -r '.rotation_weights // {}' "$CONFIG" > /tmp/rotation-weights.json
echo "blocked=$BLOCKED" >> "$GITHUB_OUTPUT"
echo "boosted=$BOOSTED" >> "$GITHUB_OUTPUT"
echo "max_issues=$MAX_ISSUES" >> "$GITHUB_OUTPUT"
echo "pr_guidance=$PR_GUIDANCE" >> "$GITHUB_OUTPUT"
# Export regression-driven focus overrides (areas with repeat issues)
FOCUS_OVERRIDES=$(jq -r '.regression_insights.focus_overrides // [] | join(",")' "$CONFIG")
echo "focus_overrides=$FOCUS_OVERRIDES" >> "$GITHUB_OUTPUT"
echo "Tuning config loaded — blocked: ${BLOCKED:-none}, boosted: ${BOOSTED:-none}, max_issues: ${MAX_ISSUES:-default}, focus_overrides: ${FOCUS_OVERRIDES:-none}"
else
echo "blocked=" >> "$GITHUB_OUTPUT"
echo "boosted=" >> "$GITHUB_OUTPUT"
echo "max_issues=0" >> "$GITHUB_OUTPUT"
echo "pr_guidance=" >> "$GITHUB_OUTPUT"
echo '{}' > /tmp/rotation-weights.json
echo "No tuning config found, running all checks"
fi
- name: Determine daily focus
id: focus
env:
FOCUS_OVERRIDE: ${{ inputs.focus_override }}
FOCUS_OVERRIDES_REGRESSION: ${{ steps.tuning.outputs.focus_overrides }}
run: |
OVERRIDE="$FOCUS_OVERRIDE"
if [ -n "$OVERRIDE" ] && [ "$OVERRIDE" != "none" ]; then
echo "area=$OVERRIDE" >> "$GITHUB_OUTPUT"
echo "Focus override: $OVERRIDE"
else
# All 8 focus areas
AREAS=("performance" "security" "a11y" "operator" "sre" "features" "resilience" "consistency")
# Parse regression-driven focus overrides (comma-separated list of areas
# where repeat issues were detected in the past 7 days)
IFS=',' read -ra REGRESSION_AREAS <<< "${FOCUS_OVERRIDES_REGRESSION:-}"
# Try weighted selection from tuning config
WEIGHTS_FILE="/tmp/rotation-weights.json"
HAS_WEIGHTS=false
if [ -f "$WEIGHTS_FILE" ] && [ "$(jq 'length' "$WEIGHTS_FILE" 2>/dev/null)" -gt 0 ]; then
HAS_WEIGHTS=true
fi
if [ "$HAS_WEIGHTS" = true ]; then
# Weighted random: build a pool where each area appears N times based on weight
# Weight 0 = skip, weight 1.0 = 10 entries, weight 2.0 = 20 entries, weight 0.5 = 5 entries
POOL=()
for AREA in "${AREAS[@]}"; do
W=$(jq -r --arg a "$AREA" '.[$a] // 1.0' "$WEIGHTS_FILE")
# Convert weight to integer pool entries (weight * 10)
ENTRIES=$(echo "$W * 10" | bc 2>/dev/null | cut -d. -f1)
ENTRIES=${ENTRIES:-10}
[ "$ENTRIES" -le 0 ] && continue
[ "$ENTRIES" -gt 20 ] && ENTRIES=20
# Boost areas flagged by regression detector (2x pool entries)
for REGAREA in "${REGRESSION_AREAS[@]}"; do
if [ "$AREA" = "$REGAREA" ]; then
ENTRIES=$((ENTRIES * 2))
[ "$ENTRIES" -gt 40 ] && ENTRIES=40
echo " Regression boost: $AREA (pool entries doubled to $ENTRIES)"
break
fi
done
for ((i=0; i<ENTRIES; i++)); do
POOL+=("$AREA")
done
done
if [ ${#POOL[@]} -gt 0 ]; then
# Use time-based seed for reproducible-per-hour selection
HOUR_SEED=$(date -u +%Y%m%d%H)
IDX=$(( ( $(echo "$HOUR_SEED" | cksum | cut -d' ' -f1) ) % ${#POOL[@]} ))
FOCUS="${POOL[$IDX]}"
echo "area=$FOCUS" >> "$GITHUB_OUTPUT"
echo "Weighted selection: $FOCUS (pool size: ${#POOL[@]}, seed: $HOUR_SEED)"
# Log weights for debugging
for AREA in "${AREAS[@]}"; do
W=$(jq -r --arg a "$AREA" '.[$a] // 1.0' "$WEIGHTS_FILE")
echo " $AREA: weight=$W"
done
if [ -n "$FOCUS_OVERRIDES_REGRESSION" ]; then
echo " Regression-boosted areas: $FOCUS_OVERRIDES_REGRESSION"
fi
else
# All weights are 0 — fall back to fixed rotation
DOY=$(date -u +%j)
SLOT=$(( 10#$DOY % 8 ))
FOCUS="${AREAS[$SLOT]}"
echo "area=$FOCUS" >> "$GITHUB_OUTPUT"
echo "All weights zero, fallback to slot $SLOT: $FOCUS"
fi
else
# No tuning config — use original fixed rotation
DOY=$(date -u +%j) # Day of year (1-366)
SLOT=$(( 10#$DOY % 8 )) # 10# forces base-10 (date +%j zero-pads, which bash reads as octal)
case $SLOT in
0) FOCUS="performance" ;;
1) FOCUS="security" ;;
2) FOCUS="a11y" ;;
3) FOCUS="operator" ;;
4) FOCUS="sre" ;;
5) FOCUS="features" ;;
6) FOCUS="resilience" ;;
7) FOCUS="consistency" ;;
esac
echo "area=$FOCUS" >> "$GITHUB_OUTPUT"
echo "Day-of-year $(date -u +%j) slot $SLOT focus: $FOCUS (no tuning weights)"
fi
fi
# ── Layer 1: Baseline Quality Checks ──────────────────────────
- name: "Check: TypeScript build"
id: build_check
working-directory: web
continue-on-error: true
run: npm run build 2>&1 | tee /tmp/build-output.txt
- name: "Check: ESLint"
id: lint_check
working-directory: web
continue-on-error: true
run: npm run lint 2>&1 | tee /tmp/lint-output.txt
- name: "Check: Go backend build"
id: go_build_check
continue-on-error: true
run: |
sudo apt-get install -y -qq gcc musl-tools > /dev/null 2>&1
CGO_ENABLED=1 go build -o /dev/null ./cmd/console 2>&1 | tee /tmp/go-build-output.txt
- name: "Check: Bundle size"
id: bundle_check
if: steps.build_check.outcome == 'success'
run: |
if [ -d "web/dist" ]; then
SIZE_KB=$(du -sk web/dist | cut -f1)
echo "size_kb=$SIZE_KB" >> "$GITHUB_OUTPUT"
if [ "$SIZE_KB" -gt "$BUNDLE_SIZE_LIMIT_KB" ]; then
echo "exceeded=true" >> "$GITHUB_OUTPUT"
echo "Bundle size ${SIZE_KB}KB exceeds limit ${BUNDLE_SIZE_LIMIT_KB}KB"
du -sh web/dist/assets/* 2>/dev/null | sort -rh | head -20 > /tmp/bundle-breakdown.txt
else
echo "exceeded=false" >> "$GITHUB_OUTPUT"
echo "Bundle size ${SIZE_KB}KB is within limit ${BUNDLE_SIZE_LIMIT_KB}KB"
fi
else
echo "exceeded=false" >> "$GITHUB_OUTPUT"
echo "No dist directory found"
fi
- name: "Check: npm audit"
id: audit_check
if: inputs.skip_audit != true
working-directory: web
run: |
AUDIT_EXIT=0
npm audit --audit-level=high --json > /tmp/audit-output.json 2>/dev/null || AUDIT_EXIT=$?
if [ "$AUDIT_EXIT" -ne 0 ] && ! jq -e '.metadata' /tmp/audit-output.json > /dev/null 2>&1; then
echo "First audit attempt failed (possibly network), retrying in 30s..."
sleep 30
npm audit --audit-level=high --json > /tmp/audit-output.json 2>/dev/null || true
fi
CRITICAL=$(jq '.metadata.vulnerabilities.critical // 0' /tmp/audit-output.json 2>/dev/null || echo "0")
HIGH=$(jq '.metadata.vulnerabilities.high // 0' /tmp/audit-output.json 2>/dev/null || echo "0")
TOTAL=$((CRITICAL + HIGH))
echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT"
echo "high=$HIGH" >> "$GITHUB_OUTPUT"
if [ "$TOTAL" -gt "0" ]; then
echo "has_vulnerabilities=true" >> "$GITHUB_OUTPUT"
echo "Found $CRITICAL critical + $HIGH high vulnerabilities"
npm audit --audit-level=high 2>/dev/null | tail -50 > /tmp/audit-summary.txt || true
else
echo "has_vulnerabilities=false" >> "$GITHUB_OUTPUT"
echo "No high/critical vulnerabilities found"
fi
# ── Layer 2: Daily Focus Checks ───────────────────────────────
# === PERFORMANCE (Monday) ===
- name: "Focus: Unused dependencies"
id: focus_unused_deps
if: steps.focus.outputs.area == 'performance'
working-directory: web
continue-on-error: true
run: |
echo "Checking for potentially unused dependencies..."
ISSUES=""
# Get all dependencies from package.json
DEPS=$(jq -r '.dependencies // {} | keys[]' package.json)
# Packages used via dynamic import(), web workers, Netlify Functions,
# peer dependencies, or build tooling — not discoverable by grepping
# src/ for static import statements. Maintain this list when adding
# new indirect deps to avoid false-positive auto-QA issues.
INDIRECT_DEPS="@netlify/blobs @sqlite.org/sqlite-wasm fflate googleapis react-is sucrase"
for dep in $DEPS; do
# Skip known framework deps that won't appear as direct imports
case "$dep" in
react|react-dom|react-scripts|vite|@vitejs/*|@types/*|typescript) continue ;;
esac
# Skip packages on the indirect-dependency allowlist
SKIP=false
for indirect in $INDIRECT_DEPS; do
if [ "$dep" = "$indirect" ]; then
SKIP=true
break
fi
done
if [ "$SKIP" = "true" ]; then
continue
fi
# Search for import/require in src/ AND netlify/functions/
COUNT=$(grep -rl "from ['\"]${dep}" src/ netlify/functions/ 2>/dev/null | wc -l || echo "0")
COUNT2=$(grep -rl "require(['\"]${dep}" src/ netlify/functions/ 2>/dev/null | wc -l || echo "0")
# Also check for dynamic import() calls
COUNT3=$(grep -rl "import(['\"]${dep}" src/ netlify/functions/ 2>/dev/null | wc -l || echo "0")
TOTAL=$((COUNT + COUNT2 + COUNT3))
if [ "$TOTAL" -eq 0 ]; then
ISSUES="${ISSUES} - \`${dep}\` — no direct imports found in src/ or netlify/functions/\n"
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Potentially unused dependencies:\n%b" "$ISSUES" > /tmp/focus-unused-deps.txt
cat /tmp/focus-unused-deps.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No unused dependencies detected"
fi
- name: "Focus: Missing lazy loading"
id: focus_lazy_loading
if: steps.focus.outputs.area == 'performance'
working-directory: web
continue-on-error: true
run: |
echo "Checking for large components that could benefit from lazy loading..."
ISSUES=""
# Find .tsx files larger than 300 lines in pages/ or views/ directories (excluding tests)
for f in $(find src -path "*/pages/*.tsx" -o -path "*/views/*.tsx" 2>/dev/null | grep -v "\.test\.tsx$" | grep -v "/__tests__/" | grep -v "\.actions\.tsx$" | grep -v "\.tabs\.tsx$" | grep -v "Section\.tsx$"); do
LINES=$(wc -l < "$f")
if [ "$LINES" -gt 300 ]; then
BASENAME=$(basename "$f")
# Check if it's already lazy-loaded (matches React.lazy, lazy, and safeLazy patterns)
LAZY=$(grep -rl -E "(React\.lazy|safeLazy|lazy).*${BASENAME%.*}" src/ 2>/dev/null | wc -l || echo "0")
if [ "$LAZY" -eq 0 ]; then
ISSUES="${ISSUES} - \`${f}\` (${LINES} lines) — not lazy-loaded\n"
fi
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Large components without lazy loading:\n%b" "$ISSUES" > /tmp/focus-lazy-loading.txt
cat /tmp/focus-lazy-loading.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No lazy loading gaps detected"
fi
- name: "Focus: Bundle chunk analysis"
id: focus_bundle_analysis
if: steps.focus.outputs.area == 'performance' && steps.build_check.outcome == 'success'
continue-on-error: true
run: |
echo "Analyzing bundle chunks for large assets..."
ISSUES=""
if [ -d "web/dist/assets" ]; then
# Find JS chunks over 300KB
LARGE_CHUNKS=$(find web/dist/assets -name "*.js" -size +300k 2>/dev/null | while IFS= read -r f; do
SIZE=$(du -sh "$f" | cut -f1)
echo " - $(basename "$f") (${SIZE})"
done | sort | head -20 || true)
if [ -n "$LARGE_CHUNKS" ]; then
ISSUES="${ISSUES}### JS chunks over 300KB\nLarge chunks increase initial load time — consider code splitting:\n${LARGE_CHUNKS}\n\n"
fi
# Top 10 largest assets
TOP_ASSETS=$(du -sh web/dist/assets/* 2>/dev/null | sort -rh | head -10 | \
awk '{gsub(/web\/dist\/assets\//, "", $2); print " - " $2 " (" $1 ")"}' || true)
if [ -n "$TOP_ASSETS" ]; then
ISSUES="${ISSUES}### Top 10 largest bundle assets\n${TOP_ASSETS}\n\n"
fi
CHUNK_COUNT=$(find web/dist/assets -name "*.js" 2>/dev/null | wc -l | tr -d ' ')
TOTAL_KB=$(du -sk web/dist 2>/dev/null | cut -f1)
ISSUES="${ISSUES}### Bundle summary\n - Total size: ${TOTAL_KB}KB\n - JS chunk count: ${CHUNK_COUNT}\n"
else
echo "No dist directory found — skipping bundle analysis"
fi
if echo "$ISSUES" | grep -q "JS chunks over"; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
if [ -n "$ISSUES" ]; then
printf "%b" "$ISSUES" > /tmp/focus-bundle-analysis.txt
cat /tmp/focus-bundle-analysis.txt
fi
- name: "Focus: Large source files"
id: focus_large_files
if: steps.focus.outputs.area == 'performance'
working-directory: web
continue-on-error: true
run: |
echo "Checking for oversized source files..."
ISSUES=""
# Find TypeScript/TSX files over 500 lines
LARGE=$(find src -name "*.ts" -o -name "*.tsx" 2>/dev/null | while IFS= read -r f; do
LINES=$(wc -l < "$f" 2>/dev/null || echo "0")
if [ "$LINES" -gt 500 ]; then
echo "${LINES} ${f}"
fi
done | sort -rn | head -20 || true)
if [ -n "$LARGE" ]; then
COUNT=$(echo "$LARGE" | wc -l | tr -d ' ')
FORMATTED=$(echo "$LARGE" | awk '{print " - `" $2 "` (" $1 " lines)"}')
ISSUES="${ISSUES}### Source files over 500 lines (${COUNT} found)\nLarge files are harder to maintain and may impact compile performance:\n${FORMATTED}\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-large-files.txt
cat /tmp/focus-large-files.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No oversized source files detected"
fi
- name: "Focus: Import cost and tree-shaking"
id: focus_import_cost
if: steps.focus.outputs.area == 'performance'
working-directory: web
continue-on-error: true
run: |
echo "Checking for import patterns that prevent tree-shaking..."
ISSUES=""
# Namespace imports prevent tree-shaking
STAR_IMPORTS=$(grep -rn "import \* as" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|\.test\." | head -15 || true)
if [ -n "$STAR_IMPORTS" ]; then
COUNT=$(echo "$STAR_IMPORTS" | wc -l | tr -d ' ')
ISSUES="${ISSUES}### Namespace imports (\`import * as\`) that prevent tree-shaking (${COUNT} found)\nUse named imports instead — e.g. \`import { specific } from 'lib'\`:\n\`\`\`\n${STAR_IMPORTS}\n\`\`\`\n\n"
fi
# Full lodash imports pull in the entire library
LODASH_FULL=$(grep -rn "from 'lodash'" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|\.test\." | head -10 || true)
if [ -n "$LODASH_FULL" ]; then
ISSUES="${ISSUES}### Full lodash import (imports entire library)\nUse per-method imports instead — e.g. \`import debounce from 'lodash/debounce'\`:\n\`\`\`\n${LODASH_FULL}\n\`\`\`\n\n"
fi
if echo "$ISSUES" | grep -q "Namespace imports\|Full lodash"; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
if [ -n "$ISSUES" ]; then
printf "%b" "$ISSUES" > /tmp/focus-import-cost.txt
cat /tmp/focus-import-cost.txt
else
echo "No import cost issues detected"
fi
# === SECURITY (Tuesday) ===
- name: "Focus: Hardcoded URLs and tokens"
id: focus_hardcoded
if: steps.focus.outputs.area == 'security'
working-directory: web
continue-on-error: true
run: |
echo "Scanning for hardcoded URLs, tokens, and secrets..."
ISSUES=""
# Look for hardcoded API URLs (not localhost, not relative)
# Exclude known-safe patterns:
# - config/externalApis.ts (contains intentional documentation URLs)
# - mocks/ directory (test/demo data only)
# - Lines with "SECURITY: Safe" comments
# - Lines with "example-org" (mock data)
# - example.com domains
URLS=$(grep -rn "https\?://[^localhost][^'\"]*api" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v node_modules | \
grep -v "\.test\." | \
grep -v "config/externalApis.ts" | \
grep -v "mocks/" | \
grep -v "SECURITY: Safe" | \
grep -v "example-org" | \
grep -v "example\.com" | \
head -20 || true)
if [ -n "$URLS" ]; then
ISSUES="${ISSUES}### Hardcoded API URLs\n\`\`\`\n${URLS}\n\`\`\`\n\n"
fi
# Look for potential secrets/tokens
# Exclude known-safe patterns:
# - mocks/handlers.ts (contains mock tokens for testing)
# - Lines with "NOT A REAL TOKEN" comments
# - Lines with "mock" in the token value
TOKENS=$(grep -rn "token\s*[:=]\s*['\"][A-Za-z0-9]" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v node_modules | \
grep -v "\.test\." | \
grep -v "mocks/handlers.ts" | \
grep -v "NOT A REAL TOKEN" | \
grep -v "mock-.*-token" | \
grep -vi "csrf" | \
grep -vi "type" | \
head -10 || true)
if [ -n "$TOKENS" ]; then
ISSUES="${ISSUES}### Potential Hardcoded Tokens\n\`\`\`\n${TOKENS}\n\`\`\`\n\n"
fi
# Look for hardcoded passwords
# Exclude known-safe patterns similar to tokens
PASSWORDS=$(grep -rn "password\s*[:=]\s*['\"][^'\"]\+" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v node_modules | \
grep -v "\.test\." | \
grep -v "mocks/" | \
grep -vi "type\|interface\|placeholder\|label\|name=" | \
head -10 || true)
if [ -n "$PASSWORDS" ]; then
ISSUES="${ISSUES}### Potential Hardcoded Passwords\n\`\`\`\n${PASSWORDS}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-hardcoded.txt
echo "Potential security issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No hardcoded credentials detected"
fi
- name: "Focus: Dependency age check"
id: focus_dep_age
if: steps.focus.outputs.area == 'security'
working-directory: web
continue-on-error: true
run: |
echo "Checking for outdated dependencies..."
npm outdated --json > /tmp/outdated.json 2>/dev/null || true
MAJOR_OUTDATED=$(jq -r 'to_entries[] | select(.value.current != .value.latest) | select((.value.current | split(".")[0]) != (.value.latest | split(".")[0])) | "\(.key): \(.value.current) → \(.value.latest)"' /tmp/outdated.json 2>/dev/null | head -20 || true)
if [ -n "$MAJOR_OUTDATED" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
echo "Dependencies with major version updates available:" > /tmp/focus-dep-age.txt
echo "$MAJOR_OUTDATED" >> /tmp/focus-dep-age.txt
cat /tmp/focus-dep-age.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No major version gaps detected"
fi
# === NAVIGATION & ACCESSIBILITY (Wednesday) ===
- name: "Focus: Missing ARIA labels"
id: focus_aria
if: steps.focus.outputs.area == 'a11y'
working-directory: web
continue-on-error: true
run: |
echo "Checking for interactive elements missing ARIA labels..."
ISSUES=""
# Find buttons without aria-label or children text
BUTTONS=$(grep -rn "<button" src/ --include="*.tsx" 2>/dev/null | grep -v "aria-label" | grep -v "aria-labelledby" | grep -v node_modules | head -20 || true)
if [ -n "$BUTTONS" ]; then
ISSUES="${ISSUES}### Buttons possibly missing ARIA labels\n\`\`\`\n${BUTTONS}\n\`\`\`\n\n"
fi
# Find icon-only buttons (button with only an icon child)
ICON_BUTTONS=$(grep -rn "<button.*>" src/ --include="*.tsx" 2>/dev/null | grep -i "icon\|svg" | grep -v "aria-label" | grep -v node_modules | head -10 || true)
if [ -n "$ICON_BUTTONS" ]; then
ISSUES="${ISSUES}### Icon-only buttons without ARIA labels\n\`\`\`\n${ICON_BUTTONS}\n\`\`\`\n\n"
fi
# Find images without alt text
IMAGES=$(grep -rn "<img" src/ --include="*.tsx" 2>/dev/null | grep -v 'alt=' | grep -v node_modules | head -10 || true)
if [ -n "$IMAGES" ]; then
ISSUES="${ISSUES}### Images without alt text\n\`\`\`\n${IMAGES}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-aria.txt
echo "Accessibility issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No ARIA label issues detected"
fi
- name: "Focus: Keyboard navigation gaps"
id: focus_keyboard
if: steps.focus.outputs.area == 'a11y'
working-directory: web
continue-on-error: true
run: |
echo "Checking for keyboard navigation issues..."
ISSUES=""
# onClick without onKeyDown/onKeyPress
CLICK_NO_KEY=$(grep -rn "onClick=" src/ --include="*.tsx" 2>/dev/null | grep -v "onKey" | grep -v "<button" | grep -v "<a " | grep -v "<input" | grep -v "<select" | grep -v node_modules | head -15 || true)
if [ -n "$CLICK_NO_KEY" ]; then
ISSUES="${ISSUES}### Click handlers without keyboard equivalents\n\`\`\`\n${CLICK_NO_KEY}\n\`\`\`\n\n"
fi
# Divs with onClick (should be buttons)
DIV_CLICK=$(grep -rn "<div.*onClick" src/ --include="*.tsx" 2>/dev/null | grep -v 'role=' | grep -v 'tabIndex' | grep -v node_modules | head -10 || true)
if [ -n "$DIV_CLICK" ]; then
ISSUES="${ISSUES}### Clickable divs without role or tabIndex\n\`\`\`\n${DIV_CLICK}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-keyboard.txt
echo "Keyboard navigation gaps found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No keyboard navigation gaps detected"
fi
- name: "Focus: Color contrast risk patterns"
id: focus_color_contrast
if: steps.focus.outputs.area == 'a11y'
working-directory: web
continue-on-error: true
run: |
echo "Checking for potential color contrast issues..."
ISSUES=""
# Light gray text on potentially light backgrounds (likely below WCAG AA 4.5:1)
LOW_CONTRAST=$(grep -rn "text-gray-[23]00\b\|text-slate-[23]00\b\|text-zinc-[23]00\b\|text-neutral-[23]00\b" src/ --include="*.tsx" 2>/dev/null | \
grep -v "dark:\|node_modules\|\.test\." | head -20 || true)
if [ -n "$LOW_CONTRAST" ]; then
COUNT=$(echo "$LOW_CONTRAST" | wc -l | tr -d ' ')
ISSUES="${ISSUES}### Light gray text classes (${COUNT} found) — may fail WCAG AA on white/light backgrounds\nGray-200/300 text on light backgrounds often falls below the 4.5:1 contrast ratio:\n\`\`\`\n${LOW_CONTRAST}\n\`\`\`\n\n"
fi
# Text with opacity that could reduce contrast below threshold
OPACITY_TEXT=$(grep -rn "text-opacity-[1-5][0-9]\b\|text-[a-z]\+-[0-9]\+\/[1-5][0-9]\b" src/ --include="*.tsx" 2>/dev/null | \
grep "text-" | grep -v "node_modules\|\.test\." | head -15 || true)
if [ -n "$OPACITY_TEXT" ]; then
ISSUES="${ISSUES}### Text with reduced opacity — may fall below WCAG AA contrast\nOpacity below 60% on colored text can cause contrast failures:\n\`\`\`\n${OPACITY_TEXT}\n\`\`\`\n\n"
fi
# Inline color styles that bypass design tokens and may not have been contrast-checked
INLINE_COLOR=$(grep -rn "color:\s*['\"]#\|color:\s*rgb" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|\.test\.\|var(--\|backgroundColor" | head -15 || true)
if [ -n "$INLINE_COLOR" ]; then
COUNT=$(echo "$INLINE_COLOR" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 3 ]; then
ISSUES="${ISSUES}### Inline color styles (${COUNT} found) — contrast not validated by design system\nUse Tailwind classes or CSS variables so contrast can be centrally verified:\n\`\`\`\n${INLINE_COLOR}\n\`\`\`\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-color-contrast.txt
echo "Color contrast risks found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No color contrast issues detected"
fi
# === OPERATOR USEFULNESS (Thursday) ===
- name: "Focus: Hardcoded thresholds and magic numbers"
id: focus_magic_numbers
if: steps.focus.outputs.area == 'operator'
working-directory: web
continue-on-error: true
run: |
echo "Checking for hardcoded thresholds and magic numbers..."
ISSUES=""
# Find numeric comparisons that look like thresholds
THRESHOLDS=$(grep -rn "[><=]\s*[0-9]\{2,\}" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." | grep -v "index\." | grep -v "z-index" | grep -v "width\|height\|padding\|margin\|size\|px\|rem\|em" | head -20 || true)
if [ -n "$THRESHOLDS" ]; then
ISSUES="${ISSUES}### Numeric thresholds that could be configurable\n\`\`\`\n${THRESHOLDS}\n\`\`\`\n\n"
fi
# Find setTimeout/setInterval with magic numbers
TIMERS=$(grep -rn "setTimeout\|setInterval" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -o "[0-9]\{4,\}" | sort -u | head -10 || true)
if [ -n "$TIMERS" ]; then
ISSUES="${ISSUES}### Timer values that could be named constants\nValues found: ${TIMERS}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-magic-numbers.txt
echo "Magic numbers found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No magic numbers detected"
fi
- name: "Focus: Missing tooltips and help text"
id: focus_tooltips
if: steps.focus.outputs.area == 'operator'
working-directory: web
continue-on-error: true
run: |
echo "Checking for technical abbreviations rendered as JSX text without a wrapping acronym/tooltip..."
ISSUES=""
# Issue 8970: the previous grep matched abbreviations anywhere
# (identifiers, state-machine strings, comparisons, type names),
# producing ~92% false positives (30-day Copilot acceptance 8%).
# Constrain to JSX text — an abbreviation appearing directly between
# > and < — which is what the user actually sees and what a tooltip
# would annotate. Skip matches already wrapped in <TechnicalAcronym>
# (the project's established affordance for acronym explanation).
ABBREV_PATTERN='CPU|RAM|OOM|CRD|RBAC|PVC|PV|HPA|VPA|SLO|SLI|SLA|MTTR|MTTF'
ABBREV=$(grep -rnP ">\s*(${ABBREV_PATTERN})\b[^<]{0,60}<" src/ --include="*.tsx" 2>/dev/null \
| grep -v node_modules \
| grep -v "\.test\." \
| grep -v "\.stories\." \
| grep -v "TechnicalAcronym" \
| head -15 || true)
if [ -n "$ABBREV" ]; then
ISSUES="${ISSUES}### Technical abbreviations rendered as JSX text without a wrapping acronym/tooltip\nWrap each in \`<TechnicalAcronym term=\"...\">\` (see \`web/src/components/ui/TechnicalAcronym.tsx\`) so hovering explains the term.\n\`\`\`\n${ABBREV}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-tooltips.txt
echo "Missing tooltips found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No tooltip gaps detected"
fi
# === SRE / MULTI-CLUSTER (Friday) ===
- name: "Focus: Single-cluster assumptions"
id: focus_single_cluster
if: steps.focus.outputs.area == 'sre'
working-directory: web
continue-on-error: true
run: |
echo "Checking for single-cluster-assumption red flags..."
ISSUES=""
# Issue 8970: the previous heuristics flagged legitimate per-cluster
# code (any .map(cluster => ...) rendering a list, any occurrence of
# the word "cluster" not followed by "s") as bugs, producing ~91%
# false positives (30-day Copilot acceptance 9%). Narrow to concrete
# red flags that reliably indicate assuming exactly one cluster:
# 1) Indexing [0] on a clusters-array variable (picking the first
# cluster silently — the known failure mode, see GitOps.tsx).
# 2) Hardcoded cluster-name comparisons against literal strings.
FIRST_CLUSTER=$(grep -rnP '\b(clusters|deduplicatedClusters|reachableClusters|activeClusters)\s*\[\s*0\s*\]' src/ --include="*.ts" --include="*.tsx" 2>/dev/null \
| grep -v node_modules \
| grep -v "\.test\." \
| grep -v "\.spec\." \
| grep -vE ":[0-9]+:\s*(//|\*)" \
| head -10 || true)
if [ -n "$FIRST_CLUSTER" ]; then
ISSUES="${ISSUES}### Picking the first cluster with \`[0]\`\nThese silently ignore all but the first cluster. Either iterate all clusters or make the choice explicit (e.g., a user-selected cluster prop).\n\`\`\`\n${FIRST_CLUSTER}\n\`\`\`\n\n"
fi
HARDCODED_CLUSTER=$(grep -rnP "clusterName\s*(===|==|!==|!=)\s*['\"][a-z][a-z0-9_-]{1,}['\"]" src/ --include="*.ts" --include="*.tsx" 2>/dev/null \
| grep -v node_modules \
| grep -v "\.test\." \
| grep -v "\.spec\." \
| head -10 || true)
if [ -n "$HARDCODED_CLUSTER" ]; then
ISSUES="${ISSUES}### Hardcoded cluster-name comparisons\nMatching against a literal cluster name breaks when the same code runs in clusters with different names.\n\`\`\`\n${HARDCODED_CLUSTER}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-single-cluster.txt
echo "Single-cluster assumptions found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No single-cluster assumptions detected"
fi
- name: "Focus: Missing health indicators"
id: focus_health
if: steps.focus.outputs.area == 'sre'
working-directory: web
continue-on-error: true
run: |
echo "Checking for missing health/status indicators in SRE views..."
ISSUES=""
# Find dashboard/overview components without health status
DASHBOARDS=$(find src -name "*Dashboard*" -o -name "*Overview*" -o -name "*Summary*" 2>/dev/null | grep -v node_modules | grep "\.tsx$")
for f in $DASHBOARDS; do
HAS_HEALTH=$(grep -l "health\|Health\|status\|Status\|alert\|Alert\|warning\|Warning" "$f" 2>/dev/null || true)
if [ -z "$HAS_HEALTH" ]; then
ISSUES="${ISSUES} - \`${f}\` — dashboard/overview without health indicators\n"
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Dashboard components missing health indicators:\n%b" "$ISSUES" > /tmp/focus-health.txt
cat /tmp/focus-health.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No missing health indicators detected"
fi
# === FEATURE RECOMMENDATIONS (Saturday) ===
- name: "Focus: TODO/FIXME/HACK comments"
id: focus_todos
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Scanning for TODO, FIXME, HACK comments..."
ISSUES=""
TODOS=$(grep -rn "TODO\|FIXME\|HACK\|XXX\|WORKAROUND" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v node_modules | head -30 || true)
if [ -n "$TODOS" ]; then
COUNT=$(echo "$TODOS" | wc -l)
echo "found=true" >> "$GITHUB_OUTPUT"
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
echo "Found ${COUNT} TODO/FIXME/HACK comments:" > /tmp/focus-todos.txt
echo "$TODOS" >> /tmp/focus-todos.txt
cat /tmp/focus-todos.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No TODO/FIXME/HACK comments found"
fi
- name: "Focus: High-complexity components"
id: focus_complexity
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Finding high-complexity components..."
ISSUES=""
# Components over 400 lines are candidates for splitting
for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules); do
LINES=$(wc -l < "$f")
if [ "$LINES" -gt 400 ]; then
# Count useState hooks as complexity indicator
HOOKS=$(grep -c "useState\|useEffect\|useCallback\|useMemo\|useRef" "$f" 2>/dev/null || echo "0")
ISSUES="${ISSUES} - \`${f}\` — ${LINES} lines, ${HOOKS} hooks\n"
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Components that could benefit from splitting:\n%b" "$ISSUES" > /tmp/focus-complexity.txt
cat /tmp/focus-complexity.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No overly complex components detected"
fi
# === CODE QUALITY (Saturday - part of features) ===
- name: "Focus: Debug console.log statements"
id: focus_console_logs
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Scanning for debug console.log statements..."
ISSUES=""
# Find console.log that looks like debugging (not error handling)
DEBUG_LOGS=$(grep -rn "console\.log" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | \
grep -v "// eslint-disable" | \
grep -v "error\|Error\|warn\|Warn\|debug mode" | \
head -20 || true)
if [ -n "$DEBUG_LOGS" ]; then
COUNT=$(echo "$DEBUG_LOGS" | wc -l | tr -d ' ')
echo "found=true" >> "$GITHUB_OUTPUT"
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
printf "Debug console.log statements found (${COUNT} occurrences):\n\`\`\`\n%s\n\`\`\`\n" "$DEBUG_LOGS" > /tmp/focus-console-logs.txt
cat /tmp/focus-console-logs.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No debug console.log statements found"
fi
- name: "Focus: Excessive any types"
id: focus_any_types
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Scanning for excessive use of 'any' type..."
ISSUES=""
# Find explicit 'any' type annotations (excluding eslint comments)
ANY_TYPES=$(grep -rn ": any\|: any\[\]\|as any\|<any>" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | \
grep -v "eslint-disable" | \
grep -v "\.d\.ts" | \
head -25 || true)
if [ -n "$ANY_TYPES" ]; then
COUNT=$(echo "$ANY_TYPES" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 10 ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
printf "Excessive 'any' type usage (${COUNT} occurrences) reduces type safety:\n\`\`\`\n%s\n\`\`\`\n" "$ANY_TYPES" > /tmp/focus-any-types.txt
cat /tmp/focus-any-types.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Only ${COUNT} 'any' types found (acceptable)"
fi
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No excessive 'any' types found"
fi
- name: "Focus: Potential memory leaks"
id: focus_memory_leaks
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Checking for potential memory leak patterns..."
ISSUES=""
# Find useEffect with setInterval/setTimeout but no cleanup
for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules); do
HAS_TIMER=$(grep -l "setInterval\|setTimeout" "$f" 2>/dev/null || true)
if [ -n "$HAS_TIMER" ]; then
HAS_CLEANUP=$(grep -l "clearInterval\|clearTimeout\|return.*=>" "$f" 2>/dev/null || true)
if [ -z "$HAS_CLEANUP" ]; then
ISSUES="${ISSUES} - \`${f}\` — uses setInterval/setTimeout without cleanup\n"
fi
fi
done
# Find addEventListener without removeEventListener
for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules); do
ADD_COUNT=$(grep -c "addEventListener" "$f" 2>/dev/null || echo "0")
REMOVE_COUNT=$(grep -c "removeEventListener" "$f" 2>/dev/null || echo "0")
if [ "$ADD_COUNT" -gt 0 ] && [ "$REMOVE_COUNT" -lt "$ADD_COUNT" ]; then
ISSUES="${ISSUES} - \`${f}\` — adds ${ADD_COUNT} event listeners but only removes ${REMOVE_COUNT}\n"
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Potential memory leak patterns:\n%b\n\nThese components may leak memory by not cleaning up timers or event listeners." "$ISSUES" > /tmp/focus-memory-leaks.txt
cat /tmp/focus-memory-leaks.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No obvious memory leak patterns detected"
fi
# === RESILIENCE & ERROR HANDLING (every run) ===
- name: "Check: Swallowed errors"
id: focus_swallowed
working-directory: web
continue-on-error: true
run: |
echo "Checking for swallowed errors and empty catch blocks..."
ISSUES=""
# Empty catch blocks
EMPTY_CATCH=$(grep -rn -A1 "catch\s*(" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -B1 "^\s*}" | grep "catch" | grep -v node_modules | head -15 || true)
if [ -n "$EMPTY_CATCH" ]; then
ISSUES="${ISSUES}### Empty catch blocks\n\`\`\`\n${EMPTY_CATCH}\n\`\`\`\n\n"
fi
# Catch with only console.log (no user feedback)
CONSOLE_CATCH=$(grep -rn -A2 "catch\s*(" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep "console\.\(log\|warn\)" | grep -v node_modules | head -15 || true)
if [ -n "$CONSOLE_CATCH" ]; then
ISSUES="${ISSUES}### Catch blocks with only console logging (no user feedback)\n\`\`\`\n${CONSOLE_CATCH}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-swallowed.txt
echo "Swallowed errors found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No swallowed errors detected"
fi
- name: "Check: Missing loading and error states"
id: focus_loading_states
working-directory: web
continue-on-error: true
run: |
echo "Checking for missing loading/error states in data-fetching components..."
ISSUES=""