Skip to content

Consolidate duplicated loading empty-state pattern into renderLoading… #2732

Consolidate duplicated loading empty-state pattern into renderLoading…

Consolidate duplicated loading empty-state pattern into renderLoading… #2732

Workflow file for this run

name: CAO Dashboard
on:
workflow_dispatch:
push:
paths:
- .github/workflows/cao.json
- .github/workflows/cao-dashboard.yml
- "*/dashboard.json"
- dashboard/**
permissions:
contents: read
concurrency:
group: central-agentic-ops-dashboard
cancel-in-progress: true
jobs:
build:
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref_name == github.event.repository.default_branch)
runs-on: ubuntu-latest
timeout-minutes: 120
outputs:
deploy: ${{ steps.deployment-policy.outputs.deploy }}
permissions:
actions: read
contents: read
steps:
- name: Checkout trusted dashboard source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.workflow_sha }}
persist-credentials: false
- name: Set up CAO dashboard runtime
uses: ./.github/actions/setup-cao-runtime # zizmor: ignore[self-repository] - local action is checked out at github.workflow_sha.
with:
bundle: dashboard
- name: Resolve dashboard deployment policy
id: deployment-policy
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const fs = require('fs')
const policy = JSON.parse(fs.readFileSync('.github/workflows/cao.json', 'utf8'))
const configuredDeploy = policy['control-plane']?.campaigns?.dashboard?.deploy
if (
configuredDeploy !== undefined
&& configuredDeploy !== null
&& typeof configuredDeploy !== 'boolean'
) {
throw new Error('control-plane.campaigns.dashboard.deploy must be a boolean')
}
const deploy = configuredDeploy ?? true
core.info(`Standalone Pages deployment: ${deploy ? 'enabled' : 'disabled'}`)
core.setOutput('deploy', String(deploy))
- name: Install dashboard build dependencies
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await exec.exec('npm', [
'ci',
'--prefix',
'dashboard/site',
'--ignore-scripts',
])
core.info('Dashboard build dependencies installed')
- name: Restore collected activity data
id: activity-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
${{ runner.temp }}/cao-activity/gh-aw-logs.sqlite
${{ runner.temp }}/cao-activity/gh-aw-logs-shards
${{ runner.temp }}/cao-activity/gh-aw-logs-runs
${{ runner.temp }}/cao-activity/gh-aw-logs-records
${{ runner.temp }}/cao-activity/payload-hashes.json
${{ runner.temp }}/cao-activity/control-settings.json
${{ runner.temp }}/cao-activity/inventory-sources.json
${{ runner.temp }}/cao-activity/agent-summary.json
${{ runner.temp }}/cao-activity/drain3_weights.json
${{ runner.temp }}/cao-activity/memory
key: cao-activity-v5-lookup-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
cao-activity-v5-
- name: Resolve fallback activity run
id: activity-artifact-run
if: steps.activity-cache.outputs.cache-matched-key == ''
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { data } = await github.rest.actions.listWorkflowRuns({
...context.repo,
workflow_id: 'cao-activity.yml',
branch: context.payload.repository.default_branch,
status: 'success',
per_page: 1,
})
const run = data.workflow_runs[0]
if (!run) {
throw new Error('No successful CAO Activity workflow run is available')
}
core.info(`Using activity artifact from workflow run ${run.id}`)
core.setOutput('run-id', String(run.id))
- name: Download fallback activity data
if: steps.activity-cache.outputs.cache-matched-key == ''
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: cao-activity-index
path: ${{ runner.temp }}/cao-activity
repository: ${{ github.repository }}
github-token: ${{ github.token }}
run-id: ${{ steps.activity-artifact-run.outputs.run-id }}
- name: Upgrade fallback activity data
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
NODE_DEBUG: cao:ingest
ACTIVITY_DATABASE: ${{ runner.temp }}/cao-activity/gh-aw-logs.sqlite
REPORT_GH_AW_LOGS_SHARDS: ${{ runner.temp }}/cao-activity/gh-aw-logs-shards
REPORT_GH_AW_LOGS_NORMALIZED: ${{ runner.temp }}/cao-activity/gh-aw-logs-normalized
REPORT_GH_AW_LOGS_RUNS: ${{ runner.temp }}/cao-activity/gh-aw-logs-runs
REPORT_GH_AW_LOGS_RECORDS: ${{ runner.temp }}/cao-activity/gh-aw-logs-records
REPORT_INVENTORY_SOURCES: ${{ runner.temp }}/cao-activity/inventory-sources.json
REPORT_PAYLOAD_HASHES: ${{ runner.temp }}/cao-activity/payload-hashes.json
with:
script: |
await exec.exec(process.execPath, [
'activity/cao.mjs',
'hash-payloads',
'--shard-dir', process.env.REPORT_GH_AW_LOGS_SHARDS,
'--normalized-dir', process.env.REPORT_GH_AW_LOGS_NORMALIZED,
'--runs-dir', process.env.REPORT_GH_AW_LOGS_RUNS,
'--records-dir', process.env.REPORT_GH_AW_LOGS_RECORDS,
'--inventory', process.env.REPORT_INVENTORY_SOURCES,
])
await exec.exec(process.execPath, [
'activity/cao.mjs',
'ingest-jsonl',
'--database', process.env.ACTIVITY_DATABASE,
'--runs-dir', process.env.REPORT_GH_AW_LOGS_RUNS,
'--records-dir', process.env.REPORT_GH_AW_LOGS_RECORDS,
'--retention-days', '30',
'--run-retention-days', '30',
])
await exec.exec(process.execPath, [
'activity/cao.mjs',
'hash-payloads',
'--database', process.env.ACTIVITY_DATABASE,
'--shard-dir', process.env.REPORT_GH_AW_LOGS_SHARDS,
'--runs-dir', process.env.REPORT_GH_AW_LOGS_RUNS,
'--records-dir', process.env.REPORT_GH_AW_LOGS_RECORDS,
'--inventory', process.env.REPORT_INVENTORY_SOURCES,
'--output', process.env.REPORT_PAYLOAD_HASHES,
])
await exec.exec(process.execPath, [
'activity/agent-summary.mjs',
process.env.REPORT_INVENTORY_SOURCES,
path.join(process.env.RUNNER_TEMP, 'cao-activity', 'agent-summary.json'),
])
- name: Validate restored activity data
env:
ACTIVITY_DATABASE: ${{ runner.temp }}/cao-activity/gh-aw-logs.sqlite
REPORT_GH_AW_LOGS_SHARDS: ${{ runner.temp }}/cao-activity/gh-aw-logs-shards
REPORT_PAYLOAD_HASHES: ${{ runner.temp }}/cao-activity/payload-hashes.json
REPORT_CONTROL_SETTINGS: ${{ runner.temp }}/cao-activity/control-settings.json
REPORT_INVENTORY_SOURCES: ${{ runner.temp }}/cao-activity/inventory-sources.json
REPORT_REPOSITORY_MEMORY: ${{ runner.temp }}/cao-activity/memory/manifest.json
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await exec.exec(process.execPath, [
'activity/cao.mjs',
'validate-activity-data',
'--database', process.env.ACTIVITY_DATABASE,
'--shard-dir', process.env.REPORT_GH_AW_LOGS_SHARDS,
'--payload-hashes', process.env.REPORT_PAYLOAD_HASHES,
'--control-settings', process.env.REPORT_CONTROL_SETTINGS,
'--inventory', process.env.REPORT_INVENTORY_SOURCES,
'--memory-manifest', process.env.REPORT_REPOSITORY_MEMORY,
])
- name: Assess activity database health
env:
ACTIVITY_DATABASE: ${{ runner.temp }}/cao-activity/gh-aw-logs.sqlite
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
core.info('Assessing activity database health')
await exec.exec(process.execPath, [
'activity/cao.mjs',
'doctor',
'--database',
process.env.ACTIVITY_DATABASE,
'--run-ttl-days',
'all',
])
core.info('Activity database health assessment completed')
- name: Assemble Dashboard Language site
env:
DASHBOARD_COMMIT_SHA: ${{ github.workflow_sha }}
REPORT_OUTPUT: ${{ runner.temp }}/central-agentic-ops-dashboard
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
const formatFileSize = (bytes) => {
const units = ['bytes', 'KiB', 'MiB', 'GiB']
const unitIndex = bytes === 0
? 0
: Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
const value = bytes / 1024 ** unitIndex
return `${unitIndex === 0 ? value : value.toFixed(1)} ${units[unitIndex]}`
}
const controlSettings = path.join(process.env.RUNNER_TEMP, 'cao-activity', 'control-settings.json')
await exec.exec('npm', [
'--prefix',
'dashboard/site',
'run',
'build',
'--',
process.env.REPORT_OUTPUT,
controlSettings,
process.env.DASHBOARD_COMMIT_SHA,
path.join(process.env.RUNNER_TEMP, 'cao-activity'),
])
core.info('Dashboard site build completed')
core.info('Copying collected activity data')
const collectedFiles = [
'agent-summary.json',
'inventory-sources.json',
'gh-aw-logs.sqlite',
]
for (const fileName of collectedFiles) {
const destination = path.join(process.env.REPORT_OUTPUT, fileName)
fs.copyFileSync(
path.join(process.env.RUNNER_TEMP, 'cao-activity', fileName),
destination,
)
core.info(`Copied ${fileName} (${formatFileSize(fs.statSync(destination).size)})`)
}
const sourceManifest = JSON.parse(fs.readFileSync(
path.join(process.env.RUNNER_TEMP, 'cao-activity', 'payload-hashes.json'),
'utf8',
))
const payloadHashes = Object.fromEntries(Object.entries(sourceManifest).filter(([name]) =>
name === 'gh-aw-logs.sqlite'
|| /^gh-aw-logs-(?:runs|records)\/[^/]+\.jsonl$/.test(name),
))
payloadHashes['gh-aw-logs.sqlite'] = crypto
.createHash('sha256')
.update(fs.readFileSync(path.join(process.env.REPORT_OUTPUT, 'gh-aw-logs.sqlite')))
.digest('hex')
payloadHashes['agent-summary.json'] = crypto
.createHash('sha256')
.update(fs.readFileSync(path.join(process.env.REPORT_OUTPUT, 'agent-summary.json')))
.digest('hex')
if (!Object.keys(payloadHashes).some((name) => name.startsWith('gh-aw-logs-runs/'))) {
throw new Error('Dashboard payload contains no compacted run-information shards')
}
const payloadManifest = path.join(process.env.REPORT_OUTPUT, 'payload-hashes.json')
fs.writeFileSync(payloadManifest, `${JSON.stringify(payloadHashes, null, 2)}\n`)
core.info(`Wrote payload-hashes.json (${formatFileSize(fs.statSync(payloadManifest).size)})`)
const memorySource = path.join(process.env.RUNNER_TEMP, 'cao-activity', 'memory')
const memoryDestination = path.join(process.env.REPORT_OUTPUT, 'memory')
fs.cpSync(memorySource, memoryDestination, { recursive: true })
core.info('Copied campaign repository memory')
// Copy only manifest-listed shards so the incremental normalization
// cache under gh-aw-logs-*/.payloads/ is never published.
for (const directory of ['gh-aw-logs-runs', 'gh-aw-logs-records']) {
fs.mkdirSync(path.join(process.env.REPORT_OUTPUT, directory), { recursive: true })
}
let publishedShardBytes = 0
let publishedShardCount = 0
for (const name of Object.keys(payloadHashes)) {
if (!name.includes('/')) continue
const destination = path.join(process.env.REPORT_OUTPUT, name)
fs.copyFileSync(
path.join(process.env.RUNNER_TEMP, 'cao-activity', name),
destination,
)
publishedShardBytes += fs.statSync(destination).size
publishedShardCount += 1
}
await exec.exec(process.execPath, [
'dashboard/site/scripts/llms.mjs',
'generate',
path.join(process.env.REPORT_OUTPUT, 'llms.txt'),
process.env.REPORT_OUTPUT,
])
await exec.exec(process.execPath, [
'dashboard/site/scripts/llms.mjs',
'validate',
process.env.REPORT_OUTPUT,
process.env.GITHUB_WORKSPACE,
])
core.info(`Copied ${publishedShardCount} activity shards `
+ `(${formatFileSize(publishedShardBytes)})`)
core.info(`Dashboard artifact assembly completed (${collectedFiles.length + 1} collected data files)`)
- name: Upload dashboard artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: central-agentic-ops-dashboard
path: ${{ runner.temp }}/central-agentic-ops-dashboard
if-no-files-found: error
include-hidden-files: true
retention-days: 1
cache:
needs: build
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Download dashboard artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: central-agentic-ops-dashboard
path: dist/cao
- name: Delete previous dashboard cache
env:
DASHBOARD_CACHE_KEY: central-agentic-ops-dashboard
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const caches = await github.paginate(github.rest.actions.getActionsCacheList, {
owner: context.repo.owner,
repo: context.repo.repo,
key: process.env.DASHBOARD_CACHE_KEY,
per_page: 100,
})
const matchingCaches = caches.filter(
(cache) => cache.key === process.env.DASHBOARD_CACHE_KEY && cache.ref === process.env.GITHUB_REF,
)
for (const cache of matchingCaches) {
core.info(`Deleting dashboard cache ID ${cache.id}`)
await github.rest.actions.deleteActionsCacheById({
owner: context.repo.owner,
repo: context.repo.repo,
cache_id: cache.id,
})
}
- name: Save dashboard cache
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: dist/cao
key: central-agentic-ops-dashboard
deploy:
needs: build
if: needs.build.outputs.deploy == 'true'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
id-token: write
pages: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Download dashboard artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: central-agentic-ops-dashboard
path: ${{ runner.temp }}/central-agentic-ops-dashboard
- name: Configure Pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
with:
enablement: false
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: ${{ runner.temp }}/central-agentic-ops-dashboard
- name: Deploy Pages
id: deployment
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
notify-failure:
needs: [build, cache, deploy]
if: ${{ always() && (needs.build.result == 'failure' || needs.cache.result == 'failure' || needs.deploy.result == 'failure') }}
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Notify current repository about CAO Dashboard failure
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const title = '[cao] CAO Dashboard workflow failure'
const legacyTitle = 'CAO Dashboard workflow failure'
const failures = [
{
code: 'CAO_DASHBOARD_BUILD_FAILED',
result: '${{ needs.build.result }}',
message: 'The dashboard build job failed while restoring activity data or assembling the dashboard artifact.',
},
{
code: 'CAO_DASHBOARD_CACHE_FAILED',
result: '${{ needs.cache.result }}',
message: 'The dashboard cache job failed while saving the dashboard artifact for reuse.',
},
{
code: 'CAO_DASHBOARD_DEPLOY_FAILED',
result: '${{ needs.deploy.result }}',
message: 'The dashboard deploy job failed while publishing the dashboard to GitHub Pages.',
},
].filter(({ result }) => result === 'failure')
if (failures.length === 0) {
core.info('No failed CAO Dashboard jobs to report')
return
}
const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'
const runUrl = `${serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
const workflowSha = process.env.GITHUB_WORKFLOW_SHA
const debugSkillUrl = `${serverUrl}/githubnext/gh-aw-cao/blob/main/skills/debug-cao/SKILL.md`
const rows = failures
.map(({ code, message, result }) => `| \`${code}\` | ${message} | ${result} |`)
.join('\n')
const body = [
'The CAO Dashboard workflow failed in this repository.',
'',
'## Failure codes',
'',
'| Code | Message | Job result |',
'| --- | --- | --- |',
rows,
'',
'## Suggested next step',
'',
'Assign this issue to an agent and ask it to fix the workflow failure using the failure code(s) above.',
'',
'---',
`Ask an agent to follow the [\`debug-cao\` skill](${debugSkillUrl}) and diagnose workflow commit \`${workflowSha}\`.`,
'',
`> Generated by [CAO Dashboard](${runUrl})`,
].join('\n')
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
...context.repo,
state: 'open',
per_page: 100,
})
const existingIssue = openIssues.find((issue) =>
!issue.pull_request && [title, legacyTitle].includes(issue.title)
)
if (existingIssue) {
if (existingIssue.title !== title) {
await github.rest.issues.update({
...context.repo,
issue_number: existingIssue.number,
title,
})
}
await github.rest.issues.createComment({
...context.repo,
issue_number: existingIssue.number,
body,
})
core.info(`Commented on existing issue #${existingIssue.number}`)
return
}
const { data: issue } = await github.rest.issues.create({
...context.repo,
title,
body,
})
core.info(`Created issue #${issue.number}`)