Skip to content

Auto-QA Agent

Auto-QA Agent #45

Workflow file for this run

name: Auto-QA Agent
# Hourly quality checks with rotating focus areas.
# Layer 1 (always): Build, lint, Go build, bundle size, npm audit
# Layer 2 (always): Resilience/error handling, Inventory consistency
# Layer 3 (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=(reserved), 7=(reserved)
#
# Issues auto-assign Copilot and feed into the 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 (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=""
# 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 (every run) ===
- name: "Check: Missing component files"
id: focus_missing_files
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: "Check: Card type registry consistency"
id: focus_card_registry
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: "Check: Route consistency"
id: focus_routes
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: "Check: Modal and drill-down consistency"
id: focus_modal_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', 'triage/accepted', 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 (every run)
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', 'triage/accepted', 'auto-qa:resilience'],
});
}
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', 'auto-qa:resilience'],
});
}
// Consistency (every run)
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', 'triage/accepted', 'auto-qa:consistency'],
});
}
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', 'triage/accepted', 'auto-qa:consistency'],
});
}
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', 'triage/accepted', 'auto-qa:consistency'],
});
}
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', 'auto-qa:consistency'],
});
}
// ── 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(`Skipping duplicate: "${check.title}" (#${duplicate.number}) — still open.`);
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}`);
// Auto-assign Copilot coding agent so no human triage is needed
const repo = `${context.repo.owner}/${context.repo.repo}`;
try {
await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: ['copilot-swe-agent[bot]'],
agent_assignment: {
target_repo: repo,
base_branch: 'main',
},
});
core.info(`Assigned Copilot to #${issue.number}`);
} catch (e) {
core.warning(`Could not assign Copilot to #${issue.number}: ${e.message}`);
}
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.`);