Skip to content

Attribute CAO validation finding issues (#14409) #4379

Attribute CAO validation finding issues (#14409)

Attribute CAO validation finding issues (#14409) #4379

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'],
})
}