Skip to content

Nightly Compliance & Perf #48

Nightly Compliance & Perf

Nightly Compliance & Perf #48

name: Nightly Compliance & Perf
# Runs all compliance and performance test suites nightly.
# On failure or performance regression, opens a GitHub issue assigned to Copilot
# with structured diagnostics for automated fixing.
#
# Suites:
# Compliance (6): card-cache, card-loading, security, i18n, a11y, interaction, error-resilience
# Performance (3): dashboard-perf (TTFI), dashboard-nav, all-cards-ttfi
#
# Uses production build (vite preview) for consistent results.
on:
schedule:
- cron: '0 5 * * *' # 5:00 UTC daily (midnight EST)
workflow_dispatch:
inputs:
skip_issue_creation:
description: 'Skip creating issues for failures'
required: false
default: false
type: boolean
env:
CI: true
NODE_VERSION: '22'
PREVIEW_PORT: 4174
permissions:
contents: read
issues: write
jobs:
build:
if: github.repository == 'kubestellar/console'
name: Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: web/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build production bundle
run: npm run build
- name: Upload build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-dist
path: web/dist
retention-days: 1
# ── Compliance Suites ────────────────────────────────────────────────
compliance:
if: github.repository == 'kubestellar/console'
name: Compliance (${{ matrix.suite }})
needs: build
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
suite:
- card-cache
- card-loading
- security
- i18n
- a11y
- interaction
- error-resilience
include:
- suite: card-cache
spec: e2e/compliance/card-cache-compliance.spec.ts
report: cache-compliance-report.json
summary: cache-compliance-summary.md
- suite: card-loading
spec: e2e/compliance/card-loading-compliance.spec.ts
report: compliance-report.json
summary: compliance-summary.md
- suite: security
spec: e2e/compliance/security-compliance.spec.ts
report: security-compliance-report.json
summary: security-compliance-summary.md
- suite: i18n
spec: e2e/compliance/i18n-compliance.spec.ts
report: i18n-compliance-report.json
summary: i18n-compliance-summary.md
- suite: a11y
spec: e2e/compliance/a11y-compliance.spec.ts
report: a11y-compliance-report.json
summary: a11y-compliance-summary.md
- suite: interaction
spec: e2e/compliance/interaction-compliance.spec.ts
report: interaction-compliance-report.json
summary: interaction-compliance-summary.md
- suite: error-resilience
spec: e2e/compliance/error-resilience.spec.ts
report: error-resilience-report.json
summary: error-resilience-summary.md
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: web/package-lock.json
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Download build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: build-dist
path: web/dist
- name: Start preview server
run: |
npx vite preview --port ${{ env.PREVIEW_PORT }} --host &
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:${{ env.PREVIEW_PORT }} > /dev/null 2>&1; then
echo "Server ready after ${i}s"
exit 0
fi
sleep 1
done
echo "::error::Preview server failed to start"
exit 1
- name: Run ${{ matrix.suite }} compliance
id: test
env:
PLAYWRIGHT_BASE_URL: http://127.0.0.1:${{ env.PREVIEW_PORT }}
run: |
npx playwright test \
--config e2e/compliance/compliance.config.ts \
${{ matrix.spec }} \
--project=chromium 2>&1 | tee /tmp/test-output.txt
echo "exit_code=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: compliance-${{ matrix.suite }}
path: |
web/e2e/test-results/${{ matrix.report }}
web/e2e/test-results/${{ matrix.summary }}
web/e2e/test-results/compliance/
retention-days: 14
- name: Save result
if: always()
run: |
mkdir -p /tmp/results
if [ "${{ steps.test.outcome }}" = "failure" ]; then
echo "FAIL" > /tmp/results/${{ matrix.suite }}.status
else
echo "PASS" > /tmp/results/${{ matrix.suite }}.status
fi
if [ -f "e2e/test-results/${{ matrix.summary }}" ]; then
cp "e2e/test-results/${{ matrix.summary }}" /tmp/results/${{ matrix.suite }}-summary.md
fi
- name: Upload result status
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: result-${{ matrix.suite }}
path: /tmp/results/
retention-days: 1
# ── Performance Suites ───────────────────────────────────────────────
perf:
if: github.repository == 'kubestellar/console'
name: Perf (${{ matrix.suite }})
needs: build
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
suite:
- ttfi
- dashboard-perf
- dashboard-nav
include:
- suite: ttfi
spec: e2e/perf/all-cards-ttfi.spec.ts
config: e2e/perf/perf.config.ts
report: ttfi-report.json
summary: ttfi-summary.md
- suite: dashboard-perf
spec: e2e/perf/dashboard-perf.spec.ts
config: e2e/perf/perf.config.ts
report: perf-report.json
summary: perf-summary.txt
- suite: dashboard-nav
spec: e2e/perf/dashboard-nav.spec.ts
config: e2e/perf/perf.config.ts
report: nav-report.json
summary: nav-summary.md
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: web/package-lock.json
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Download build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: build-dist
path: web/dist
- name: Start preview server
run: |
npx vite preview --port ${{ env.PREVIEW_PORT }} --host &
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:${{ env.PREVIEW_PORT }} > /dev/null 2>&1; then
echo "Server ready after ${i}s"
exit 0
fi
sleep 1
done
echo "::error::Preview server failed to start"
exit 1
- name: Run ${{ matrix.suite }} perf test
id: test
env:
PLAYWRIGHT_BASE_URL: http://127.0.0.1:${{ env.PREVIEW_PORT }}
run: |
npx playwright test \
--config ${{ matrix.config }} \
${{ matrix.spec }} \
--project=chromium 2>&1 | tee /tmp/test-output.txt
echo "exit_code=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT
- name: Run TTFI baseline comparison
if: matrix.suite == 'ttfi' && always()
run: node e2e/perf/compare-ttfi.mjs || true
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: perf-${{ matrix.suite }}
path: |
web/e2e/test-results/${{ matrix.report }}
web/e2e/test-results/${{ matrix.summary }}
web/e2e/test-results/ttfi-regression.md
retention-days: 14
- name: Save result
if: always()
run: |
mkdir -p /tmp/results
if [ "${{ steps.test.outcome }}" = "failure" ]; then
echo "FAIL" > /tmp/results/${{ matrix.suite }}.status
else
echo "PASS" > /tmp/results/${{ matrix.suite }}.status
fi
if [ -f "e2e/test-results/${{ matrix.summary }}" ]; then
cp "e2e/test-results/${{ matrix.summary }}" /tmp/results/${{ matrix.suite }}-summary.md
fi
# Include TTFI regression report if present
if [ -f "e2e/test-results/ttfi-regression.md" ]; then
cp "e2e/test-results/ttfi-regression.md" /tmp/results/ttfi-regression.md
fi
- name: Upload result status
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: result-${{ matrix.suite }}
path: /tmp/results/
retention-days: 1
# ── Report & Issue Creation ──────────────────────────────────────────
# Creates one focused issue PER failed suite (not one combined issue).
# Each issue is assigned to Copilot with auto-qa labels so it can
# independently create a PR to fix that specific suite.
# When a suite that previously failed now passes, its issue is auto-closed.
report:
name: Report & Create Issues
needs: [compliance, perf]
if: always() && github.repository == 'kubestellar/console'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download all results
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: result-*
path: /tmp/all-results
merge-multiple: true
- name: Aggregate results
id: aggregate
run: |
SUITES=(card-cache card-loading security i18n a11y interaction error-resilience ttfi dashboard-perf dashboard-nav)
FAILED=""
PASSED=""
for suite in "${SUITES[@]}"; do
status_file="/tmp/all-results/${suite}.status"
if [ -f "$status_file" ]; then
status=$(cat "$status_file")
if [ "$status" = "FAIL" ]; then
FAILED="${FAILED}${suite},"
else
PASSED="${PASSED}${suite},"
fi
else
FAILED="${FAILED}${suite},"
fi
done
FAILED="${FAILED%,}"
PASSED="${PASSED%,}"
echo "failed=$FAILED" >> $GITHUB_OUTPUT
echo "passed=$PASSED" >> $GITHUB_OUTPUT
TOTAL=${#SUITES[@]}
FAIL_COUNT=0
PASS_COUNT=0
if [ -n "$FAILED" ]; then FAIL_COUNT=$(echo "$FAILED" | tr ',' '\n' | wc -l | tr -d ' '); fi
if [ -n "$PASSED" ]; then PASS_COUNT=$(echo "$PASSED" | tr ',' '\n' | wc -l | tr -d ' '); fi
echo "total=$TOTAL" >> $GITHUB_OUTPUT
echo "fail_count=$FAIL_COUNT" >> $GITHUB_OUTPUT
echo "pass_count=$PASS_COUNT" >> $GITHUB_OUTPUT
echo "has_failures=$( [ -n "$FAILED" ] && echo true || echo false )" >> $GITHUB_OUTPUT
# Step summary
echo "## Nightly Compliance & Perf Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Date**: $(date -u '+%Y-%m-%d') | **Passed**: $PASS_COUNT / $TOTAL" >> $GITHUB_STEP_SUMMARY
if [ -n "$FAILED" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Failures" >> $GITHUB_STEP_SUMMARY
for suite in $(echo "$FAILED" | tr ',' ' '); do
echo "- **$suite**" >> $GITHUB_STEP_SUMMARY
done
fi
if [ -n "$PASSED" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Passed" >> $GITHUB_STEP_SUMMARY
for suite in $(echo "$PASSED" | tr ',' ' '); do
echo "- $suite" >> $GITHUB_STEP_SUMMARY
done
fi
- name: Create per-suite issues for failures & auto-close passing
if: github.event.inputs.skip_issue_creation != 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const fs = require('fs');
const failed = '${{ steps.aggregate.outputs.failed }}'.split(',').filter(Boolean);
const passed = '${{ steps.aggregate.outputs.passed }}'.split(',').filter(Boolean);
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const date = new Date().toISOString().split('T')[0];
const owner = context.repo.owner;
const repo = context.repo.repo;
// ── Suite metadata for structured issue bodies ──
const suiteMeta = {
'card-cache': {
type: 'compliance',
spec: 'e2e/compliance/card-cache-compliance.spec.ts',
config: 'e2e/compliance/compliance.config.ts',
title: 'Card Cache Compliance',
hint: [
'Cards showing demo data on warm return means the cache hydration has a bug.',
'Look for `initialData: { isDemo: true }` patterns in hooks — the `isDemoFallback`',
'must be suppressed while `cacheResult.isLoading` is true.',
'Check that `useCardLoadingState({ isDemoData })` is wired correctly.',
].join('\n'),
},
'card-loading': {
type: 'compliance',
spec: 'e2e/compliance/card-loading-compliance.spec.ts',
config: 'e2e/compliance/compliance.config.ts',
title: 'Card Loading Compliance',
hint: [
'Cards must transition from skeleton → content without getting stuck.',
'Ensure `useCardLoadingState` receives correct `isLoading` and `isDemoData` props.',
'Check that cards set `data-loading="false"` once content is available.',
].join('\n'),
},
'security': {
type: 'compliance',
spec: 'e2e/compliance/security-compliance.spec.ts',
config: 'e2e/compliance/compliance.config.ts',
title: 'Security Compliance',
hint: [
'Check for XSS vectors in user-facing inputs, missing CSP headers,',
'exposed secrets in HTML/JS, and insecure cookie settings.',
'Fix in source code — do NOT weaken the test assertions.',
].join('\n'),
},
'i18n': {
type: 'compliance',
spec: 'e2e/compliance/i18n-compliance.spec.ts',
config: 'e2e/compliance/compliance.config.ts',
title: 'i18n Compliance',
hint: [
'Hardcoded English strings found in the UI that should use `t()` translations.',
'Extract strings to `web/public/locales/en/translation.json` and wrap with `useTranslation`.',
'Buttons, headings, and labels are failures; tooltips and descriptions are warnings.',
].join('\n'),
},
'a11y': {
type: 'compliance',
spec: 'e2e/compliance/a11y-compliance.spec.ts',
config: 'e2e/compliance/compliance.config.ts',
title: 'Accessibility (a11y) Compliance',
hint: [
'WCAG 2.1 AA violations found by axe-core.',
'Fix contrast ratios, add aria-labels to interactive elements,',
'ensure focus management works for keyboard navigation.',
].join('\n'),
},
'interaction': {
type: 'compliance',
spec: 'e2e/compliance/interaction-compliance.spec.ts',
config: 'e2e/compliance/compliance.config.ts',
title: 'Interaction Compliance',
hint: [
'Core UI interactions broken: search (Cmd+K), theme toggle, card expand/collapse,',
'sidebar collapse, or dashboard refresh.',
'Check event handlers and component state management.',
].join('\n'),
},
'error-resilience': {
type: 'compliance',
spec: 'e2e/compliance/error-resilience.spec.ts',
config: 'e2e/compliance/compliance.config.ts',
title: 'Error Resilience',
hint: [
'UI does not gracefully handle API failures (500s, timeouts, partial outages).',
'Cards should show error states — not blank screens or infinite skeletons.',
'Check error boundaries and fallback UI in card components.',
].join('\n'),
},
'ttfi': {
type: 'perf',
spec: 'e2e/perf/all-cards-ttfi.spec.ts',
config: 'e2e/perf/perf.config.ts',
title: 'Time-To-First-Interactive (TTFI)',
hint: [
'Cards are taking too long to show first content.',
'Profile with Chrome DevTools Performance tab.',
'Check for unnecessary re-renders, large bundle imports, or slow data hooks.',
'Compare against baseline in `e2e/perf/baseline/`.',
].join('\n'),
},
'dashboard-perf': {
type: 'perf',
spec: 'e2e/perf/dashboard-perf.spec.ts',
config: 'e2e/perf/perf.config.ts',
title: 'Dashboard Card Performance',
hint: [
'Dashboard card loading times exceed thresholds.',
'Check for heavy components that should be lazy-loaded,',
'unnecessary data fetching on mount, or chart rendering bottlenecks.',
].join('\n'),
},
'dashboard-nav': {
type: 'perf',
spec: 'e2e/perf/dashboard-nav.spec.ts',
config: 'e2e/perf/perf.config.ts',
title: 'Dashboard Navigation Performance',
hint: [
'Navigation between dashboards is too slow.',
'Check KeepAliveOutlet cache behavior, route transitions,',
'and card mount/render times. Back-navigation should hit cache.',
].join('\n'),
},
};
// ── Ensure labels exist ──
const labelDefs = {
'auto-qa': { color: 'b60205', description: 'Automated QA — assigned to Copilot for auto-fix' },
'nightly-compliance': { color: '1d76db', description: 'From nightly compliance/perf test run' },
'copilot': { color: 'd4c5f9', description: 'Copilot coding agent should fix this' },
'bug': { color: 'd73a4a', description: 'Something is broken' },
};
for (const [name, def] of Object.entries(labelDefs)) {
try {
await github.rest.issues.getLabel({ owner, repo, name });
} catch {
try {
await github.rest.issues.createLabel({ owner, repo, name, color: def.color, description: def.description });
core.info(`Created label: ${name}`);
} catch (e) {
core.warning(`Could not create label ${name}: ${e.message}`);
}
}
}
// ── Fetch all open nightly issues (for dedup and auto-close) ──
const openIssues = await github.rest.issues.listForRepo({
owner, repo, state: 'open', labels: 'nightly-compliance', per_page: 100,
});
// ── Auto-close issues for suites that now pass ──
for (const suite of passed) {
const tag = `[nightly:${suite}]`;
const existing = openIssues.data.find(i => i.title.includes(tag));
if (existing) {
await github.rest.issues.createComment({
owner, repo, issue_number: existing.number,
body: `## Resolved — ${date}\n\nThis suite now passes. [Workflow run](${runUrl})\n\nAuto-closing.`,
});
await github.rest.issues.update({
owner, repo, issue_number: existing.number, state: 'closed',
});
core.info(`Auto-closed #${existing.number} — ${suite} now passes`);
}
}
// ── Create or update issues for failed suites ──
for (const suite of failed) {
const meta = suiteMeta[suite] || {
type: 'unknown', spec: `e2e/**/${suite}*.spec.ts`,
config: 'e2e/compliance/compliance.config.ts',
title: suite, hint: 'Check the test output for details.',
};
// Read summary
let summary = '_No summary available_';
try {
const p = `/tmp/all-results/${suite}-summary.md`;
if (fs.existsSync(p)) summary = fs.readFileSync(p, 'utf8').slice(0, 4000);
} catch {}
// TTFI regression report (if applicable)
let regression = '';
if (suite === 'ttfi') {
try {
const p = '/tmp/all-results/ttfi-regression.md';
if (fs.existsSync(p)) regression = '\n\n### Regression Report\n\n' + fs.readFileSync(p, 'utf8').slice(0, 2000);
} catch {}
}
const tag = `[nightly:${suite}]`;
const title = `${tag} ${meta.title} failed — ${date}`;
const runCmd = meta.type === 'compliance'
? `cd web && PLAYWRIGHT_BASE_URL=http://localhost:5174 npx playwright test --config ${meta.config} ${meta.spec} --project=chromium`
: `cd web && PLAYWRIGHT_BASE_URL=http://localhost:5174 npx playwright test --config ${meta.config} ${meta.spec} --project=chromium`;
const body = [
`## ${meta.title} — Nightly Failure`,
'',
`**Date**: ${date} | **Suite**: \`${suite}\` | [Workflow run](${runUrl})`,
'',
'## What Failed',
'',
'<details>',
'<summary>Test summary (click to expand)</summary>',
'',
'```',
summary,
'```',
'',
'</details>',
regression,
'',
'## How to Fix',
'',
'### Run locally',
'',
'```bash',
runCmd,
'```',
'',
'### Fix guidance',
'',
meta.hint,
'',
'### Rules',
'',
'1. Fix the root cause in **source code** — do NOT weaken test assertions',
'2. Re-run the suite locally and verify **0 failures** before submitting',
`3. The spec file is \`web/${meta.spec}\` — read it to understand what's being checked`,
'4. Check recent commits that may have introduced the regression',
'',
'---',
`*Auto-created by [Nightly Compliance workflow](${runUrl}). Assigned to Copilot for auto-fix.*`,
].join('\n');
const labels = ['auto-qa', 'nightly-compliance', 'copilot', 'bug'];
// Check for existing open issue for this suite
const existing = openIssues.data.find(i => i.title.includes(tag));
if (existing) {
await github.rest.issues.createComment({
owner, repo, issue_number: existing.number,
body: `## Still failing — ${date}\n\n[Workflow run](${runUrl})\n\n<details>\n<summary>Latest summary</summary>\n\n\`\`\`\n${summary}\n\`\`\`\n\n</details>`,
});
core.info(`Updated existing issue #${existing.number} for ${suite}`);
continue;
}
// Create new issue
const { data: issue } = await github.rest.issues.create({
owner, repo, title, body, labels,
});
core.info(`Created issue #${issue.number}: ${title}`);
// Assign Copilot coding agent
try {
await github.rest.issues.addAssignees({
owner, repo, issue_number: issue.number, assignees: ['copilot'],
});
core.info(`Assigned Copilot to #${issue.number}`);
} catch (e1) {
core.warning(`copilot assignee failed: ${e1.message} — trying copilot-swe-agent[bot]`);
try {
await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
owner, repo, issue_number: issue.number,
assignees: ['copilot-swe-agent[bot]'],
});
core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`);
} catch (e2) {
core.warning(`Could not assign Copilot to #${issue.number}: ${e2.message}`);
}
}
}
- name: All green
if: steps.aggregate.outputs.has_failures == 'false'
run: |
echo "## All Suites Passed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All ${{ steps.aggregate.outputs.total }} compliance and performance suites passed." >> $GITHUB_STEP_SUMMARY
echo "No issues created. Any previously open nightly issues have been auto-closed." >> $GITHUB_STEP_SUMMARY