Attribute CAO validation finding issues (#14409) #4379
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: Dashboard views | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| schedule: | |
| - cron: "53 5 * * *" | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: dashboard-views | |
| cancel-in-progress: false | |
| jobs: | |
| assess: | |
| name: Assess dashboard views | |
| if: >- | |
| github.event_name != 'pull_request' || | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Cache Chromium | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ hashFiles('package-lock.json') }}-chromium | |
| - name: Install Chromium | |
| # Playwright's system dependencies come from the Ubuntu archives. The runner | |
| # image also configures Google's Chrome repository, whose index intermittently | |
| # fails checksum verification and breaks `apt-get update`; CI never installs | |
| # from it, so drop it before resolving dependencies. | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/google-chrome.list /etc/apt/sources.list.d/google-chrome.sources | |
| npx playwright install --with-deps chromium | |
| - name: Select affected pull request views | |
| id: selection | |
| if: github.event_name == 'pull_request' | |
| run: >- | |
| node tests/e2e/dashboard-view-selection.mjs | |
| "${{ github.event.pull_request.base.sha }}" | |
| "${{ github.event.pull_request.head.sha }}" | |
| >> "$GITHUB_OUTPUT" | |
| - name: Assess dashboard views | |
| id: assessment | |
| continue-on-error: true | |
| env: | |
| DASHBOARD_DATA_URL: https://githubnext.github.io/gh-aw-cao/cao/payload-hashes.json | |
| DASHBOARD_VIEWS_OUTPUT_DIR: test-results/dashboard-views | |
| EVENT_NAME: ${{ github.event_name }} | |
| SELECTED_PAGE_IDS: ${{ steps.selection.outputs.page-ids }} | |
| run: | | |
| if [ "$EVENT_NAME" = "pull_request" ]; then | |
| export DASHBOARD_PAGE_IDS="$SELECTED_PAGE_IDS" | |
| else | |
| unset DASHBOARD_PAGE_IDS | |
| fi | |
| npm run test:e2e:dashboard-views | |
| - name: Upload assessment | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: dashboard-views | |
| path: test-results/dashboard-views/summary.json | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| report: | |
| name: Report dashboard view assessment | |
| if: >- | |
| always() && | |
| (github.event_name != 'pull_request' || | |
| github.event.pull_request.head.repo.full_name == github.repository) | |
| needs: assess | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| actions: read | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Download assessment | |
| id: download | |
| continue-on-error: true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: dashboard-views | |
| path: assessment | |
| - name: Report assessment | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const fs = require('fs') | |
| const marker = '<!-- dashboard-views -->' | |
| const summaryPath = 'assessment/summary.json' | |
| const runUrl = | |
| `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` | |
| const isPullRequest = context.eventName === 'pull_request' | |
| const safeIdentifier = (value) => | |
| String(value ?? '').replace(/[^A-Za-z0-9._-]/g, '?').slice(0, 80) | |
| const safeSummaryText = (value) => | |
| String(value ?? '').replace(/[\r\n|]+/g, ' ').trim().slice(0, 500) | |
| let body | |
| if ('${{ steps.download.outcome }}' !== 'success' || !fs.existsSync(summaryPath)) { | |
| core.info(`Dashboard view assessment summary is unavailable at ${summaryPath}.`) | |
| body = [ | |
| marker, | |
| '### Dashboard view assessment', | |
| '', | |
| 'The informational assessment did not produce a summary.', | |
| '', | |
| `[View the workflow run](${runUrl}).`, | |
| ].join('\n') | |
| } else { | |
| const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')) | |
| const results = Array.isArray(summary.results) ? summary.results : [] | |
| const rows = results.map((result) => { | |
| const findings = [ | |
| ...(result.crashed ? ['browser crash'] : []), | |
| ...((result.errors || []).length > 0 | |
| ? [`${result.errors.length} browser error(s)`] | |
| : []), | |
| ...((result.assessmentErrors || []).length > 0 | |
| ? [`${result.assessmentErrors.length} assessment error(s)`] | |
| : []), | |
| ...((result.failedRequests || []).length > 0 | |
| ? [`${result.failedRequests.length} failed request(s)`] | |
| : []), | |
| ...((result.missingViews || []).length > 0 | |
| ? [`${result.missingViews.length} missing view(s)`] | |
| : []), | |
| ...((result.missingData || []).length > 0 | |
| ? [`${result.missingData.length} missing data source(s)`] | |
| : []), | |
| ...((result.loadingViews || []).length > 0 | |
| ? [`${result.loadingViews.length} loading skeleton(s)`] | |
| : []), | |
| ...(result.domNodes > summary.maximumDomNodes | |
| ? [`DOM budget exceeded: ${result.domNodes}/${summary.maximumDomNodes}`] | |
| : []), | |
| ] | |
| const pageId = safeIdentifier(result.pageId) | |
| const status = ['passed', 'failed', 'incomplete'].includes(result.status) | |
| ? result.status | |
| : 'unknown' | |
| const domNodes = Number.isFinite(result.domNodes) ? result.domNodes : 'unknown' | |
| core.info( | |
| `${pageId}: ${status}; ${domNodes} DOM nodes; ` + | |
| `${findings.length} finding(s)` | |
| ) | |
| return `| ${pageId} | ${status} | ${domNodes} | ${findings.join('; ') || 'None'} |` | |
| }) | |
| const passed = results.filter((result) => result.status === 'passed').length | |
| const blocker = summary.blocker ? safeSummaryText(summary.blocker) : '' | |
| const queryUsageGraph = | |
| typeof summary.queryUsageGraph === 'string' | |
| ? summary.queryUsageGraph.replaceAll('```', '` ` `').trim().slice(0, 40_000) | |
| : '' | |
| const queryUsageGraphSection = queryUsageGraph ? [ | |
| '<details><summary><b>View-query graph</b></summary>', | |
| '', | |
| '```mermaid', | |
| queryUsageGraph, | |
| '```', | |
| '', | |
| '</details>', | |
| '', | |
| ].join('\n') : '' | |
| const resultsSection = rows.length > 0 ? [ | |
| '<details><summary><b>View assessment results</b></summary>', | |
| '', | |
| '| View | Result | DOM nodes | Findings |', | |
| '|---|---:|---:|---|', | |
| ...rows, | |
| '', | |
| '</details>', | |
| '', | |
| ].join('\n') : '' | |
| body = [ | |
| marker, | |
| '### Dashboard view assessment', | |
| '', | |
| results.length === 0 | |
| ? isPullRequest | |
| ? 'No dashboard views were potentially impacted by this pull request.' | |
| : 'No dashboard views were assessed.' | |
| : `**${passed}/${results.length}** ${isPullRequest ? 'selected ' : ''}views passed the informational assessment. ` + | |
| `The DOM budget is **${summary.maximumDomNodes}** nodes per loaded view.`, | |
| blocker ? `Assessment blocker: ${blocker}` : '', | |
| Array.isArray(summary.ignoredPageIds) && summary.ignoredPageIds.length > 0 | |
| ? `Ignored views: ${summary.ignoredPageIds.map(safeIdentifier).join(', ')}.` | |
| : '', | |
| '', | |
| resultsSection, | |
| '', | |
| queryUsageGraphSection, | |
| '', | |
| `[View the workflow run](${runUrl}).`, | |
| ].filter(Boolean).join('\n') | |
| } | |
| if (isPullRequest) { | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| ...context.repo, | |
| issue_number: context.issue.number, | |
| per_page: 100, | |
| }) | |
| const existing = comments.find((comment) => | |
| comment.user?.type === 'Bot' && comment.body?.includes(marker) | |
| ) | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| ...context.repo, | |
| comment_id: existing.id, | |
| body, | |
| }) | |
| } else { | |
| await github.rest.issues.createComment({ | |
| ...context.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }) | |
| } | |
| } else { | |
| body = [ | |
| body, | |
| '', | |
| '**Action:** Assign this issue to Copilot using the agent prompt below; accept the fix when the affected views pass the assessment.', | |
| '', | |
| '<details><summary><b>Agent prompt</b></summary>', | |
| '', | |
| 'Investigate and fix the dashboard view assessment findings reported in this issue.', | |
| '', | |
| `1. Download the \`dashboard-views\` artifact from ${runUrl} and inspect \`summary.json\` for the affected page IDs, browser errors, failed requests, missing views or data sources, and DOM-budget violations.`, | |
| '2. Reproduce the findings from the repository root with `DASHBOARD_DATA_URL=https://githubnext.github.io/gh-aw-cao/cao/payload-hashes.json npm run test:e2e:dashboard-views`; use `DASHBOARD_PAGE_IDS` to narrow the run when appropriate.', | |
| '3. Trace each failure through the declarative view, canonical data adapter and normalization, query worker, and view payload stages. Do not add a JavaScript-based dashboard view or treat browser IndexedDB as an authority.', | |
| '4. Make the smallest root-cause fix and add or update focused regression coverage.', | |
| '5. Validate the affected assessment plus `npm test`, `npm run lint`, and `npm run typecheck` from `dashboard/site/`.', | |
| '', | |
| '</details>', | |
| '', | |
| ].join('\n') | |
| const issues = await github.paginate(github.rest.issues.listForRepo, { | |
| ...context.repo, | |
| state: 'open', | |
| per_page: 100, | |
| }) | |
| const previous = issues.filter((issue) => | |
| !issue.pull_request && | |
| issue.user?.type === 'Bot' && | |
| issue.body?.includes(marker) | |
| ) | |
| for (const issue of previous) { | |
| await github.graphql( | |
| `mutation($input: CloseIssueInput!) { | |
| closeIssue(input: $input) { | |
| issue { id } | |
| } | |
| }`, | |
| { | |
| input: { | |
| issueId: issue.node_id, | |
| stateReason: 'COMPLETED', | |
| }, | |
| }, | |
| ) | |
| } | |
| await github.rest.issues.create({ | |
| ...context.repo, | |
| title: 'Dashboard view assessment', | |
| body, | |
| labels: ['self-care'], | |
| }) | |
| } |