Nightly Compliance & Perf #48
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: 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 |