Auto-QA Agent #37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Auto-QA Agent | |
| # Hourly quality checks with rotating focus areas. | |
| # Layer 1 (always): Build, lint, Go build, bundle size, npm audit | |
| # Layer 2 (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/Error Handling, | |
| # 7=Inventory Consistency (cross-refs INVENTORY.md + consistency.md) | |
| # | |
| # Issues feed into the Copilot automation pipeline. | |
| on: | |
| schedule: | |
| - cron: '17 * * * *' # Every hour at :17 (offset from :00 to reduce congestion) | |
| 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|none)' | |
| required: false | |
| default: '' | |
| type: string | |
| env: | |
| BUNDLE_SIZE_LIMIT_KB: 5120 | |
| MAX_ISSUES_PER_RUN: 3 | |
| ISSUE_PREFIX: "[Auto-QA]" | |
| NODE_VERSION: "20" | |
| GO_VERSION: "1.23" | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| auto-qa: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| # ── Setup ────────────────────────────────────────────────────── | |
| - name: Checkout main | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| - name: Install frontend dependencies | |
| working-directory: web | |
| run: npm ci | |
| - name: Determine daily focus | |
| id: focus | |
| run: | | |
| OVERRIDE="${{ inputs.focus_override }}" | |
| if [ -n "$OVERRIDE" ] && [ "$OVERRIDE" != "none" ]; then | |
| echo "area=$OVERRIDE" >> "$GITHUB_OUTPUT" | |
| echo "Focus override: $OVERRIDE" | |
| else | |
| 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 $DOY slot $SLOT focus: $FOCUS" | |
| 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) | |
| 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 | |
| # Search for import/require of this dep in src/ | |
| COUNT=$(grep -rl "from ['\"]${dep}" src/ 2>/dev/null | wc -l || echo "0") | |
| COUNT2=$(grep -rl "require(['\"]${dep}" src/ 2>/dev/null | wc -l || echo "0") | |
| TOTAL=$((COUNT + COUNT2)) | |
| if [ "$TOTAL" -eq 0 ]; then | |
| ISSUES="${ISSUES} - \`${dep}\` — no direct imports found in src/\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 | |
| for f in $(find src -path "*/pages/*.tsx" -o -path "*/views/*.tsx" 2>/dev/null); do | |
| LINES=$(wc -l < "$f") | |
| if [ "$LINES" -gt 300 ]; then | |
| BASENAME=$(basename "$f") | |
| # Check if it's already lazy-loaded | |
| LAZY=$(grep -rl "React.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 | |
| # === 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) | |
| URLS=$(grep -rn "https\?://[^localhost][^'\"]*api" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." | 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 | |
| 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 -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 | |
| PASSWORDS=$(grep -rn "password\s*[:=]\s*['\"][^'\"]\+" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." | 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 | |
| # === 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 UI elements that could benefit from tooltips..." | |
| ISSUES="" | |
| # Find abbreviated or technical text without tooltips | |
| ABBREV=$(grep -rn "CPU\|RAM\|OOM\|CRD\|RBAC\|PVC\|PV\b\|HPA\|VPA\|SLO\|SLI\|SLA\|MTTR\|MTTF" src/ --include="*.tsx" 2>/dev/null | grep -v "tooltip\|Tooltip\|title=" | grep -v node_modules | grep -v "\.test\." | head -15 || true) | |
| if [ -n "$ABBREV" ]; then | |
| ISSUES="${ISSUES}### Technical abbreviations without tooltips\n\`\`\`\n${ABBREV}\n\`\`\`\n\n" | |
| fi | |
| # Find status indicators without explanation | |
| STATUS=$(grep -rn "status\|Status" src/ --include="*.tsx" 2>/dev/null | grep -i "badge\|chip\|indicator\|dot" | grep -v "tooltip\|Tooltip\|title=" | grep -v node_modules | head -10 || true) | |
| if [ -n "$STATUS" ]; then | |
| ISSUES="${ISSUES}### Status indicators without tooltips\n\`\`\`\n${STATUS}\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 assumptions..." | |
| ISSUES="" | |
| # Find hardcoded cluster references | |
| SINGLE=$(grep -rn "cluster\b" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v "clusters\|Clusters\|multi\|Multi\|clusterName\|clusterId\|ClusterList\|ClusterCard" | grep -vi "type\|interface\|import" | grep -v node_modules | grep -v "\.test\." | head -15 || true) | |
| if [ -n "$SINGLE" ]; then | |
| ISSUES="${ISSUES}### References that may assume single cluster\n\`\`\`\n${SINGLE}\n\`\`\`\n\n" | |
| fi | |
| # Find missing aggregation (forEach without reduce/map across clusters) | |
| NO_AGG=$(grep -rn "\.forEach\|\.map\|\.filter" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -i "cluster\|namespace" | grep -v "\.reduce\|aggregate\|total\|sum" | grep -v node_modules | head -10 || true) | |
| if [ -n "$NO_AGG" ]; then | |
| ISSUES="${ISSUES}### Cluster iterations without aggregation\n\`\`\`\n${NO_AGG}\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 | |
| # === RESILIENCE & ERROR HANDLING (Sunday) === | |
| - name: "Focus: Swallowed errors" | |
| id: focus_swallowed | |
| if: steps.focus.outputs.area == 'resilience' | |
| 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: "Focus: Missing loading and error states" | |
| id: focus_loading_states | |
| if: steps.focus.outputs.area == 'resilience' | |
| working-directory: web | |
| continue-on-error: true | |
| run: | | |
| echo "Checking for missing loading/error states in data-fetching components..." | |
| ISSUES="" | |
| # Find fetch/useEffect with data fetching but no loading state | |
| for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules); do | |
| HAS_FETCH=$(grep -l "fetch\|axios\|useQuery\|useSWR\|useEffect.*fetch\|api\." "$f" 2>/dev/null || true) | |
| if [ -n "$HAS_FETCH" ]; then | |
| HAS_LOADING=$(grep -l "loading\|Loading\|isLoading\|spinner\|Spinner\|skeleton\|Skeleton" "$f" 2>/dev/null || true) | |
| HAS_ERROR=$(grep -l "error\|Error\|isError\|errorMessage\|ErrorBoundary" "$f" 2>/dev/null || true) | |
| MISSING="" | |
| [ -z "$HAS_LOADING" ] && MISSING="loading" | |
| [ -z "$HAS_ERROR" ] && MISSING="${MISSING:+$MISSING, }error" | |
| if [ -n "$MISSING" ]; then | |
| ISSUES="${ISSUES} - \`${f}\` — fetches data but missing ${MISSING} state\n" | |
| fi | |
| fi | |
| done | |
| if [ -n "$ISSUES" ]; then | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| printf "Components fetching data without proper loading/error states:\n%b" "$ISSUES" > /tmp/focus-loading-states.txt | |
| cat /tmp/focus-loading-states.txt | |
| else | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| echo "No missing loading/error states detected" | |
| fi | |
| # === INVENTORY CONSISTENCY (8th rotation) === | |
| - name: "Focus: Missing component files" | |
| id: focus_missing_files | |
| if: steps.focus.outputs.area == 'consistency' | |
| working-directory: web | |
| continue-on-error: true | |
| run: | | |
| echo "Cross-referencing INVENTORY.md component files against source..." | |
| ISSUES="" | |
| # Extract component file paths from INVENTORY.md | |
| if [ -f "../INVENTORY.md" ]; then | |
| # Dashboard components section — extract .tsx/.ts paths | |
| PATHS=$(grep -oE 'web/src/[^ ]+\.(tsx|ts)' ../INVENTORY.md | sed 's|^web/||' || true) | |
| MISSING="" | |
| FOUND=0 | |
| TOTAL=0 | |
| for p in $PATHS; do | |
| TOTAL=$((TOTAL + 1)) | |
| if [ ! -f "$p" ]; then | |
| MISSING="${MISSING} - \`${p}\` — listed in INVENTORY.md but file not found\n" | |
| else | |
| FOUND=$((FOUND + 1)) | |
| fi | |
| done | |
| if [ -n "$MISSING" ]; then | |
| ISSUES="${ISSUES}### Component files listed in INVENTORY.md but missing from source\n${MISSING}\n" | |
| fi | |
| ISSUES="${ISSUES}### Summary: ${FOUND}/${TOTAL} listed component files exist\n\n" | |
| else | |
| ISSUES="${ISSUES}### INVENTORY.md not found in repo root\n\n" | |
| fi | |
| # Check that page component files referenced exist | |
| PAGES=$(grep -oE '[A-Z][A-Za-z]+\.tsx' ../INVENTORY.md 2>/dev/null | sort -u || true) | |
| PAGE_MISSING="" | |
| for page in $PAGES; do | |
| MATCH=$(find src -name "$page" 2>/dev/null | head -1) | |
| if [ -z "$MATCH" ]; then | |
| PAGE_MISSING="${PAGE_MISSING} - \`${page}\` — referenced in INVENTORY.md but not found in src/\n" | |
| fi | |
| done | |
| if [ -n "$PAGE_MISSING" ]; then | |
| ISSUES="${ISSUES}### Page components referenced but not found\n${PAGE_MISSING}\n" | |
| fi | |
| if [ -n "$ISSUES" ]; then | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| printf "%b" "$ISSUES" > /tmp/focus-missing-files.txt | |
| echo "Consistency issues found" | |
| else | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| echo "All INVENTORY.md references verified" | |
| fi | |
| - name: "Focus: Card type registry consistency" | |
| id: focus_card_registry | |
| if: steps.focus.outputs.area == 'consistency' | |
| working-directory: web | |
| continue-on-error: true | |
| run: | | |
| echo "Cross-referencing INVENTORY.md card types against source code..." | |
| ISSUES="" | |
| if [ -f "../INVENTORY.md" ]; then | |
| # Extract card type identifiers from INVENTORY.md (backtick-wrapped values in Type column) | |
| CARD_TYPES=$(grep -oE '`[a-z_]+`' ../INVENTORY.md | tr -d '`' | sort -u || true) | |
| UNREGISTERED="" | |
| for ct in $CARD_TYPES; do | |
| # Skip non-card-type patterns (like field names) | |
| case "$ct" in | |
| total|score|healthy|warning|critical|*_util|firing|pending|resolved|bound|installed|*_cost) continue ;; | |
| esac | |
| # Search for this card type string in source | |
| FOUND=$(grep -rl "'${ct}'\|\"${ct}\"" src/ 2>/dev/null | head -1 || true) | |
| if [ -z "$FOUND" ]; then | |
| UNREGISTERED="${UNREGISTERED} - \`${ct}\` — in INVENTORY.md but not found in source code\n" | |
| fi | |
| done | |
| if [ -n "$UNREGISTERED" ]; then | |
| ISSUES="${ISSUES}### Card types in INVENTORY.md not found in source\n${UNREGISTERED}\n" | |
| fi | |
| fi | |
| if [ -n "$ISSUES" ]; then | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| printf "%b" "$ISSUES" > /tmp/focus-card-registry.txt | |
| echo "Card registry inconsistencies found" | |
| else | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| echo "All card types verified in source" | |
| fi | |
| - name: "Focus: Route consistency" | |
| id: focus_routes | |
| if: steps.focus.outputs.area == 'consistency' | |
| working-directory: web | |
| continue-on-error: true | |
| run: | | |
| echo "Cross-referencing INVENTORY.md routes against router config..." | |
| ISSUES="" | |
| if [ -f "../INVENTORY.md" ]; then | |
| # Extract route paths from INVENTORY.md | |
| ROUTES=$(grep -oE '`/[a-z-]+`' ../INVENTORY.md | tr -d '`' | sort -u || true) | |
| # Find the router file | |
| ROUTER_FILE=$(find src -name "App.tsx" -o -name "Router.tsx" -o -name "routes.tsx" -o -name "AppRoutes.tsx" 2>/dev/null | head -1) | |
| if [ -n "$ROUTER_FILE" ]; then | |
| MISSING_ROUTES="" | |
| for route in $ROUTES; do | |
| FOUND=$(grep -l "\"${route}\"\|'${route}'" "$ROUTER_FILE" 2>/dev/null || true) | |
| if [ -z "$FOUND" ]; then | |
| # Also check all tsx files for route definition | |
| FOUND2=$(grep -rl "path.*['\"]${route}['\"]" src/ --include="*.tsx" 2>/dev/null | head -1 || true) | |
| if [ -z "$FOUND2" ]; then | |
| MISSING_ROUTES="${MISSING_ROUTES} - \`${route}\` — listed in INVENTORY.md but not found in router\n" | |
| fi | |
| fi | |
| done | |
| if [ -n "$MISSING_ROUTES" ]; then | |
| ISSUES="${ISSUES}### Routes in INVENTORY.md not found in router config\n${MISSING_ROUTES}\n" | |
| fi | |
| else | |
| ISSUES="${ISSUES}### Could not locate router file to verify routes\n\n" | |
| fi | |
| fi | |
| if [ -n "$ISSUES" ]; then | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| printf "%b" "$ISSUES" > /tmp/focus-routes.txt | |
| echo "Route inconsistencies found" | |
| else | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| echo "All routes verified" | |
| fi | |
| - name: "Focus: Modal and drill-down consistency" | |
| id: focus_modal_consistency | |
| if: steps.focus.outputs.area == 'consistency' | |
| working-directory: web | |
| continue-on-error: true | |
| run: | | |
| echo "Cross-referencing INVENTORY.md modals/drill-downs against source..." | |
| ISSUES="" | |
| if [ -f "../INVENTORY.md" ]; then | |
| # Extract modal/drill-down file names from INVENTORY.md | |
| MODAL_FILES=$(grep -oE '[A-Z][A-Za-z]+Modal\.tsx\|[A-Z][A-Za-z]+Dialog\.tsx\|[A-Z][A-Za-z]+DrillDown\.tsx' ../INVENTORY.md | sort -u || true) | |
| MISSING_MODALS="" | |
| for modal in $MODAL_FILES; do | |
| FOUND=$(find src -name "$modal" 2>/dev/null | head -1) | |
| if [ -z "$FOUND" ]; then | |
| MISSING_MODALS="${MISSING_MODALS} - \`${modal}\` — listed in INVENTORY.md but not found\n" | |
| fi | |
| done | |
| if [ -n "$MISSING_MODALS" ]; then | |
| ISSUES="${ISSUES}### Modals/dialogs/drill-downs in INVENTORY.md but missing\n${MISSING_MODALS}\n" | |
| fi | |
| # Check for drill-down views in source not listed in INVENTORY.md | |
| ACTUAL_DRILLDOWNS=$(find src -path "*/drilldown/views/*.tsx" -exec basename {} \; 2>/dev/null | sort || true) | |
| UNLISTED="" | |
| for dd in $ACTUAL_DRILLDOWNS; do | |
| LISTED=$(grep -c "$dd" ../INVENTORY.md 2>/dev/null || echo "0") | |
| if [ "$LISTED" -eq 0 ]; then | |
| UNLISTED="${UNLISTED} - \`${dd}\` — exists in source but not listed in INVENTORY.md\n" | |
| fi | |
| done | |
| if [ -n "$UNLISTED" ]; then | |
| ISSUES="${ISSUES}### Drill-down views in source but not in INVENTORY.md\n${UNLISTED}\n" | |
| fi | |
| fi | |
| # Also check consistency.md for noted issues that may still be open | |
| if [ -f "../consistency.md" ]; then | |
| NOTED=$(grep -c "Noted\|Missing\|FIXED" ../consistency.md 2>/dev/null || echo "0") | |
| ISSUES="${ISSUES}### consistency.md status: ${NOTED} noted items found\n" | |
| fi | |
| if [ -n "$ISSUES" ]; then | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| printf "%b" "$ISSUES" > /tmp/focus-modal-consistency.txt | |
| echo "Modal/drill-down inconsistencies found" | |
| else | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| echo "All modals and drill-downs verified" | |
| fi | |
| # ── Ensure Labels Exist ──────────────────────────────────────── | |
| - name: Ensure labels exist | |
| env: | |
| GH_TOKEN: ${{ secrets.CONSOLE_AUTO }} | |
| run: | | |
| LABELS=( | |
| "auto-qa:FF6B35:Issue detected by automated QA" | |
| "auto-qa:performance:3B82F6:Performance improvement" | |
| "auto-qa:security:EF4444:Security issue" | |
| "auto-qa:a11y:A855F7:Accessibility improvement" | |
| "auto-qa:operator:F59E0B:Operator experience improvement" | |
| "auto-qa:sre:10B981:SRE/multi-cluster improvement" | |
| "auto-qa:features:8B5CF6:Feature recommendation" | |
| "auto-qa:resilience:F97316:Resilience/error handling improvement" | |
| "auto-qa:consistency:6B7280:Inventory/documentation consistency" | |
| ) | |
| for entry in "${LABELS[@]}"; do | |
| IFS=: read -r NAME COLOR DESC <<< "$entry" | |
| gh label create "$NAME" \ | |
| --repo "${{ github.repository }}" \ | |
| --color "$COLOR" \ | |
| --description "$DESC" \ | |
| 2>/dev/null || true | |
| done | |
| # ── Issue Creation ────────────────────────────────────────────── | |
| - name: Create issues for failures | |
| uses: actions/github-script@v7 | |
| env: | |
| BUILD_OUTCOME: ${{ steps.build_check.outcome }} | |
| LINT_OUTCOME: ${{ steps.lint_check.outcome }} | |
| GO_BUILD_OUTCOME: ${{ steps.go_build_check.outcome }} | |
| BUNDLE_EXCEEDED: ${{ steps.bundle_check.outputs.exceeded }} | |
| BUNDLE_SIZE_KB: ${{ steps.bundle_check.outputs.size_kb }} | |
| HAS_VULNS: ${{ steps.audit_check.outputs.has_vulnerabilities }} | |
| VULN_CRITICAL: ${{ steps.audit_check.outputs.critical }} | |
| VULN_HIGH: ${{ steps.audit_check.outputs.high }} | |
| FOCUS_AREA: ${{ steps.focus.outputs.area }} | |
| # Performance focus | |
| FOCUS_UNUSED_DEPS: ${{ steps.focus_unused_deps.outputs.found }} | |
| FOCUS_LAZY_LOADING: ${{ steps.focus_lazy_loading.outputs.found }} | |
| # Security focus | |
| FOCUS_HARDCODED: ${{ steps.focus_hardcoded.outputs.found }} | |
| FOCUS_DEP_AGE: ${{ steps.focus_dep_age.outputs.found }} | |
| # A11y focus | |
| FOCUS_ARIA: ${{ steps.focus_aria.outputs.found }} | |
| FOCUS_KEYBOARD: ${{ steps.focus_keyboard.outputs.found }} | |
| # Operator focus | |
| FOCUS_MAGIC_NUMBERS: ${{ steps.focus_magic_numbers.outputs.found }} | |
| FOCUS_TOOLTIPS: ${{ steps.focus_tooltips.outputs.found }} | |
| # SRE focus | |
| FOCUS_SINGLE_CLUSTER: ${{ steps.focus_single_cluster.outputs.found }} | |
| FOCUS_HEALTH: ${{ steps.focus_health.outputs.found }} | |
| # Features focus | |
| FOCUS_TODOS: ${{ steps.focus_todos.outputs.found }} | |
| FOCUS_TODO_COUNT: ${{ steps.focus_todos.outputs.count }} | |
| FOCUS_COMPLEXITY: ${{ steps.focus_complexity.outputs.found }} | |
| # Resilience focus | |
| FOCUS_SWALLOWED: ${{ steps.focus_swallowed.outputs.found }} | |
| FOCUS_LOADING_STATES: ${{ steps.focus_loading_states.outputs.found }} | |
| # Consistency focus | |
| FOCUS_MISSING_FILES: ${{ steps.focus_missing_files.outputs.found }} | |
| FOCUS_CARD_REGISTRY: ${{ steps.focus_card_registry.outputs.found }} | |
| FOCUS_ROUTES: ${{ steps.focus_routes.outputs.found }} | |
| FOCUS_MODAL_CONSISTENCY: ${{ steps.focus_modal_consistency.outputs.found }} | |
| # Control | |
| COMMIT_SHA: ${{ github.sha }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| github-token: ${{ secrets.CONSOLE_AUTO }} | |
| script: | | |
| const fs = require('fs'); | |
| const prefix = process.env.ISSUE_PREFIX; | |
| const maxIssues = parseInt(process.env.MAX_ISSUES_PER_RUN); | |
| const sha = process.env.COMMIT_SHA.substring(0, 7); | |
| const fullSha = process.env.COMMIT_SHA; | |
| const runUrl = process.env.RUN_URL; | |
| const focusArea = process.env.FOCUS_AREA; | |
| const now = new Date().toISOString(); | |
| function readOutput(path, lines = 80) { | |
| try { | |
| const content = fs.readFileSync(path, 'utf8'); | |
| return content.split('\n').slice(-lines).join('\n').trim(); | |
| } catch { | |
| return '(output not available)'; | |
| } | |
| } | |
| function buildBody(checkName, command, output, fixes) { | |
| return [ | |
| `## Auto-QA: ${checkName} Failure`, | |
| '', | |
| `**Detected:** ${now} | **Commit:** \`${sha}\` | **Run:** [View](${runUrl})`, | |
| '', | |
| '### Reproduction', | |
| '```bash', | |
| `git checkout ${fullSha}`, | |
| command, | |
| '```', | |
| '', | |
| '### Error Output', | |
| '```', | |
| output, | |
| '```', | |
| '', | |
| '### How to Fix', | |
| ...fixes.map(f => `- ${f}`), | |
| '', | |
| '---', | |
| `*This issue was automatically created by the [Auto-QA workflow](${runUrl}).*`, | |
| '*Labels `ai-fix-requested` and `help wanted` enable Copilot to fix this after triage.*', | |
| ].join('\n'); | |
| } | |
| function buildFocusBody(areaName, checkName, output, fixes, isEnhancement) { | |
| return [ | |
| `## Auto-QA [${areaName}]: ${checkName}`, | |
| '', | |
| `**Detected:** ${now} | **Focus:** ${areaName} | **Commit:** \`${sha}\` | **Run:** [View](${runUrl})`, | |
| '', | |
| '### Findings', | |
| '```', | |
| output, | |
| '```', | |
| '', | |
| `### ${isEnhancement ? 'Suggested Improvements' : 'How to Fix'}`, | |
| ...fixes.map(f => `- ${f}`), | |
| '', | |
| '---', | |
| `*This issue was automatically created by the [Auto-QA workflow](${runUrl}) during **${areaName}** focus day.*`, | |
| '*Labels `ai-fix-requested` and `help wanted` enable Copilot to fix this after triage.*', | |
| ].join('\n'); | |
| } | |
| // ── Collect baseline failures ── | |
| const checks = []; | |
| if (process.env.BUILD_OUTCOME === 'failure') { | |
| checks.push({ | |
| title: `${prefix} TypeScript build failure on main`, | |
| body: buildBody('TypeScript Build', 'cd web && npm run build', | |
| readOutput('/tmp/build-output.txt'), | |
| ['Check the TypeScript errors shown above', | |
| 'Fix type errors, missing imports, or syntax issues', | |
| 'Run `cd web && npm run build` to verify the fix']), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted'], | |
| }); | |
| } | |
| if (process.env.LINT_OUTCOME === 'failure') { | |
| checks.push({ | |
| title: `${prefix} ESLint violations on main`, | |
| body: buildBody('ESLint', 'cd web && npm run lint', | |
| readOutput('/tmp/lint-output.txt'), | |
| ['Fix the ESLint errors listed above', | |
| 'Common fixes: remove unused imports, add missing hook dependencies', | |
| 'Run `cd web && npm run lint` to verify the fix']), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted'], | |
| }); | |
| } | |
| if (process.env.GO_BUILD_OUTCOME === 'failure') { | |
| checks.push({ | |
| title: `${prefix} Go backend build failure on main`, | |
| body: buildBody('Go Build', 'CGO_ENABLED=1 go build -o /dev/null ./cmd/console', | |
| readOutput('/tmp/go-build-output.txt'), | |
| ['Fix the Go compilation errors shown above', | |
| 'Check for missing dependencies, type errors, or undefined references', | |
| 'Run `go build ./cmd/console` to verify the fix']), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted'], | |
| }); | |
| } | |
| if (process.env.BUNDLE_EXCEEDED === 'true') { | |
| const sizeKB = process.env.BUNDLE_SIZE_KB; | |
| const limitKB = process.env.BUNDLE_SIZE_LIMIT_KB || '5120'; | |
| checks.push({ | |
| title: `${prefix} Bundle size exceeds ${limitKB}KB (currently ${sizeKB}KB)`, | |
| body: [ | |
| `## Auto-QA: Bundle Size Exceeded`, | |
| '', `**Detected:** ${now} | **Commit:** \`${sha}\` | **Run:** [View](${runUrl})`, | |
| '', `The production bundle size is **${sizeKB}KB**, exceeding the **${limitKB}KB** threshold.`, | |
| '', '### Largest Assets', '```', | |
| readOutput('/tmp/bundle-breakdown.txt', 20), | |
| '```', '', | |
| '### How to Fix', | |
| '- Check for recently added large dependencies', | |
| '- Use dynamic imports (`React.lazy`) for heavy components', | |
| '- Review bundle with `npx vite-bundle-visualizer`', | |
| '- If the size increase is intentional, update `BUNDLE_SIZE_LIMIT_KB` in `.github/workflows/auto-qa.yml`', | |
| '', '---', | |
| `*This issue was automatically created by the [Auto-QA workflow](${runUrl}).*`, | |
| '*Labels `ai-fix-requested` and `help wanted` enable Copilot to fix this after triage.*', | |
| ].join('\n'), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted'], | |
| }); | |
| } | |
| if (process.env.HAS_VULNS === 'true') { | |
| const critical = process.env.VULN_CRITICAL || '0'; | |
| const high = process.env.VULN_HIGH || '0'; | |
| checks.push({ | |
| title: `${prefix} npm audit: ${critical} critical, ${high} high vulnerabilities`, | |
| body: [ | |
| `## Auto-QA: Security Vulnerabilities`, | |
| '', `**Detected:** ${now} | **Commit:** \`${sha}\` | **Run:** [View](${runUrl})`, | |
| '', `Found **${critical} critical** and **${high} high** severity vulnerabilities.`, | |
| '', '### Audit Summary', '```', | |
| readOutput('/tmp/audit-summary.txt', 50), | |
| '```', '', | |
| '### How to Fix', | |
| '- Run `cd web && npm audit fix` to auto-fix compatible updates', | |
| '- For breaking changes, run `cd web && npm audit fix --force` (test thoroughly)', | |
| '- Check advisories for manual remediation steps', | |
| '- Run `cd web && npm audit --audit-level=high` to verify the fix', | |
| '', '---', | |
| `*This issue was automatically created by the [Auto-QA workflow](${runUrl}).*`, | |
| '*Labels `ai-fix-requested` and `help wanted` enable Copilot to fix this after triage.*', | |
| ].join('\n'), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted'], | |
| }); | |
| } | |
| // ── Collect focus-area findings ── | |
| const focusLabels = { | |
| performance: 'auto-qa:performance', | |
| security: 'auto-qa:security', | |
| a11y: 'auto-qa:a11y', | |
| operator: 'auto-qa:operator', | |
| sre: 'auto-qa:sre', | |
| features: 'auto-qa:features', | |
| resilience: 'auto-qa:resilience', | |
| consistency: 'auto-qa:consistency', | |
| }; | |
| const focusLabel = focusLabels[focusArea] || 'auto-qa'; | |
| // Performance (Monday) | |
| if (focusArea === 'performance') { | |
| if (process.env.FOCUS_UNUSED_DEPS === 'true') { | |
| checks.push({ | |
| title: `${prefix} Potentially unused npm dependencies`, | |
| body: buildFocusBody('Performance', 'Unused Dependencies', | |
| readOutput('/tmp/focus-unused-deps.txt'), | |
| ['Remove unused dependencies with `npm uninstall <pkg>`', | |
| 'Verify each package is truly unused before removing', | |
| 'Some packages may be used indirectly (plugins, configs)'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_LAZY_LOADING === 'true') { | |
| checks.push({ | |
| title: `${prefix} Large components missing lazy loading`, | |
| body: buildFocusBody('Performance', 'Missing Lazy Loading', | |
| readOutput('/tmp/focus-lazy-loading.txt'), | |
| ['Wrap large page components with `React.lazy(() => import(...))`', | |
| 'Add `<Suspense fallback={<Loading />}>` around lazy components', | |
| 'Focus on route-level components first for maximum impact'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| } | |
| // Security (Tuesday) | |
| if (focusArea === 'security') { | |
| if (process.env.FOCUS_HARDCODED === 'true') { | |
| checks.push({ | |
| title: `${prefix} Hardcoded URLs or potential credentials in source`, | |
| body: buildFocusBody('Security', 'Hardcoded Credentials/URLs', | |
| readOutput('/tmp/focus-hardcoded.txt'), | |
| ['Move API URLs to environment variables or config files', | |
| 'Remove any hardcoded tokens or passwords', | |
| 'Use `.env` files for environment-specific values', | |
| 'Ensure no real credentials are committed (check git history)'], false), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_DEP_AGE === 'true') { | |
| checks.push({ | |
| title: `${prefix} Dependencies with major version updates available`, | |
| body: buildFocusBody('Security', 'Outdated Dependencies', | |
| readOutput('/tmp/focus-dep-age.txt'), | |
| ['Review changelogs for breaking changes before updating', | |
| 'Update one dependency at a time and test thoroughly', | |
| 'Major version bumps may require code changes'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| } | |
| // A11y (Wednesday) | |
| if (focusArea === 'a11y') { | |
| if (process.env.FOCUS_ARIA === 'true') { | |
| checks.push({ | |
| title: `${prefix} Interactive elements missing ARIA labels`, | |
| body: buildFocusBody('Accessibility', 'Missing ARIA Labels', | |
| readOutput('/tmp/focus-aria.txt'), | |
| ['Add `aria-label` to icon-only buttons', | |
| 'Add `alt` text to all images', | |
| 'Use semantic HTML elements where possible', | |
| 'Test with a screen reader to verify'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_KEYBOARD === 'true') { | |
| checks.push({ | |
| title: `${prefix} Keyboard navigation gaps`, | |
| body: buildFocusBody('Accessibility', 'Keyboard Navigation Gaps', | |
| readOutput('/tmp/focus-keyboard.txt'), | |
| ['Replace clickable `<div>` elements with `<button>` or add `role="button"` and `tabIndex={0}`', | |
| 'Add `onKeyDown` handlers alongside `onClick` for non-button elements', | |
| 'Ensure all interactive elements are focusable via Tab'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| } | |
| // Operator (Thursday) | |
| if (focusArea === 'operator') { | |
| if (process.env.FOCUS_MAGIC_NUMBERS === 'true') { | |
| checks.push({ | |
| title: `${prefix} Hardcoded thresholds and magic numbers`, | |
| body: buildFocusBody('Operator Usefulness', 'Magic Numbers', | |
| readOutput('/tmp/focus-magic-numbers.txt'), | |
| ['Extract magic numbers to named constants at the top of the file', | |
| 'Consider making thresholds configurable via environment or props', | |
| 'Add comments explaining the reasoning behind threshold values'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_TOOLTIPS === 'true') { | |
| checks.push({ | |
| title: `${prefix} Technical abbreviations without tooltips`, | |
| body: buildFocusBody('Operator Usefulness', 'Missing Tooltips', | |
| readOutput('/tmp/focus-tooltips.txt'), | |
| ['Add tooltips to technical abbreviations (CPU, RBAC, CRD, etc.)', | |
| 'Use the existing Tooltip component or HTML `title` attribute', | |
| 'Explain what status indicators mean on hover'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| } | |
| // SRE (Friday) | |
| if (focusArea === 'sre') { | |
| if (process.env.FOCUS_SINGLE_CLUSTER === 'true') { | |
| checks.push({ | |
| title: `${prefix} Code may assume single-cluster deployment`, | |
| body: buildFocusBody('SRE/Multi-Cluster', 'Single-Cluster Assumptions', | |
| readOutput('/tmp/focus-single-cluster.txt'), | |
| ['Review flagged code for multi-cluster compatibility', | |
| 'Ensure cluster selectors and filters work with multiple clusters', | |
| 'Add cluster context to API calls where missing'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_HEALTH === 'true') { | |
| checks.push({ | |
| title: `${prefix} Dashboard components missing health indicators`, | |
| body: buildFocusBody('SRE/Multi-Cluster', 'Missing Health Indicators', | |
| readOutput('/tmp/focus-health.txt'), | |
| ['Add health/status indicators to dashboard components', | |
| 'Show cluster connectivity status, degraded/healthy state', | |
| 'Add visual alerts for unhealthy resources'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| } | |
| // Features (Saturday) | |
| if (focusArea === 'features') { | |
| if (process.env.FOCUS_TODOS === 'true') { | |
| const count = process.env.FOCUS_TODO_COUNT || '?'; | |
| checks.push({ | |
| title: `${prefix} ${count} TODO/FIXME/HACK comments need attention`, | |
| body: buildFocusBody('Feature Recommendations', 'TODO/FIXME/HACK Comments', | |
| readOutput('/tmp/focus-todos.txt'), | |
| ['Review each TODO and either implement or create a tracking issue', | |
| 'Remove stale TODOs that are no longer relevant', | |
| 'Convert HACKs to proper implementations'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_COMPLEXITY === 'true') { | |
| checks.push({ | |
| title: `${prefix} High-complexity components could be split`, | |
| body: buildFocusBody('Feature Recommendations', 'Complex Components', | |
| readOutput('/tmp/focus-complexity.txt'), | |
| ['Extract sub-components from large files', | |
| 'Move hooks into custom hooks for reusability', | |
| 'Consider splitting into container + presentational components'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| } | |
| // Resilience (Sunday) | |
| if (focusArea === 'resilience') { | |
| if (process.env.FOCUS_SWALLOWED === 'true') { | |
| checks.push({ | |
| title: `${prefix} Swallowed errors and empty catch blocks`, | |
| body: buildFocusBody('Resilience', 'Swallowed Errors', | |
| readOutput('/tmp/focus-swallowed.txt'), | |
| ['Add proper error handling in empty catch blocks', | |
| 'Show user-facing error messages instead of just logging', | |
| 'Use error boundaries for React component errors', | |
| 'At minimum, log errors with `console.error` for debugging'], false), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_LOADING_STATES === 'true') { | |
| checks.push({ | |
| title: `${prefix} Components fetching data without loading/error states`, | |
| body: buildFocusBody('Resilience', 'Missing Loading/Error States', | |
| readOutput('/tmp/focus-loading-states.txt'), | |
| ['Add loading indicators (skeleton screens or spinners) while data loads', | |
| 'Add error states with retry buttons for failed fetches', | |
| 'Consider using a shared data-fetching wrapper or custom hook'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| } | |
| // Consistency (8th rotation) | |
| if (focusArea === 'consistency') { | |
| if (process.env.FOCUS_MISSING_FILES === 'true') { | |
| checks.push({ | |
| title: `${prefix} INVENTORY.md references missing component files`, | |
| body: buildFocusBody('Inventory Consistency', 'Missing Component Files', | |
| readOutput('/tmp/focus-missing-files.txt'), | |
| ['Remove stale entries from INVENTORY.md for deleted components', | |
| 'Add missing component files if they should exist', | |
| 'Run the consistency check locally to verify: `grep -oE "web/src/[^ ]+.tsx" INVENTORY.md`'], false), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_CARD_REGISTRY === 'true') { | |
| checks.push({ | |
| title: `${prefix} Card types in INVENTORY.md not registered in source`, | |
| body: buildFocusBody('Inventory Consistency', 'Unregistered Card Types', | |
| readOutput('/tmp/focus-card-registry.txt'), | |
| ['Register missing card types in the card registry', | |
| 'Remove obsolete card types from INVENTORY.md', | |
| 'Ensure card type strings match between inventory and source'], false), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_ROUTES === 'true') { | |
| checks.push({ | |
| title: `${prefix} Routes in INVENTORY.md not found in router config`, | |
| body: buildFocusBody('Inventory Consistency', 'Missing Routes', | |
| readOutput('/tmp/focus-routes.txt'), | |
| ['Add missing routes to the router configuration', | |
| 'Remove obsolete routes from INVENTORY.md', | |
| 'Verify route paths match between inventory and App.tsx'], false), | |
| labels: ['bug', 'ai-fix-requested', 'help wanted', 'auto-qa', focusLabel], | |
| }); | |
| } | |
| if (process.env.FOCUS_MODAL_CONSISTENCY === 'true') { | |
| checks.push({ | |
| title: `${prefix} Modal/drill-down inventory out of sync with source`, | |
| body: buildFocusBody('Inventory Consistency', 'Modal & Drill-Down Drift', | |
| readOutput('/tmp/focus-modal-consistency.txt'), | |
| ['Update INVENTORY.md to reflect current modals and drill-down views', | |
| 'Add new drill-down views to the inventory', | |
| 'Remove references to deleted modals/dialogs'], true), | |
| labels: ['enhancement', 'ai-fix-requested', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel], | |
| }); | |
| } | |
| } | |
| // ── Create issues ── | |
| if (checks.length === 0) { | |
| core.info(`All checks passed (focus: ${focusArea}). No issues to create.`); | |
| return; | |
| } | |
| core.info(`${checks.length} finding(s) detected (focus: ${focusArea}). Processing...`); | |
| const { data: existingIssues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'auto-qa', | |
| per_page: 100, | |
| }); | |
| let created = 0; | |
| for (const check of checks) { | |
| if (created >= maxIssues) { | |
| core.warning(`Rate limit reached (${maxIssues} issues). Remaining findings skipped.`); | |
| break; | |
| } | |
| const duplicate = existingIssues.find(i => i.title === check.title); | |
| if (duplicate) { | |
| core.info(`Duplicate: "${check.title}" (#${duplicate.number}). Adding re-detection comment.`); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: duplicate.number, | |
| body: `**Re-detected** at ${now} on commit \`${sha}\` (focus: ${focusArea}).\n\n[Workflow run](${runUrl})`, | |
| }); | |
| continue; | |
| } | |
| const { data: issue } = await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: check.title, | |
| body: check.body, | |
| labels: check.labels, | |
| }); | |
| core.info(`Created issue #${issue.number}: ${check.title}`); | |
| created++; | |
| // Auto-triage is handled by including triage/accepted in the issue labels at creation | |
| } | |
| core.info(`Auto-QA complete (focus: ${focusArea}): ${created} issue(s) created, ${checks.length} finding(s) total.`); |