Consolidate duplicated loading empty-state pattern into renderLoading… #2732
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: 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}`) |